WIP huawei subscriptions
This commit is contained in:
parent
b69b30a030
commit
df56eaa226
15 changed files with 1264 additions and 737 deletions
1
OsmAnd/.gitignore
vendored
1
OsmAnd/.gitignore
vendored
|
@ -17,6 +17,7 @@ libs/huawei-*.jar
|
||||||
huaweidrmlib/
|
huaweidrmlib/
|
||||||
HwDRM_SDK_*
|
HwDRM_SDK_*
|
||||||
drm_strings.xml
|
drm_strings.xml
|
||||||
|
agconnect-services.json
|
||||||
|
|
||||||
# copy_widget_icons.sh
|
# copy_widget_icons.sh
|
||||||
res/drawable-large/map_*
|
res/drawable-large/map_*
|
||||||
|
|
|
@ -2,24 +2,24 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<application>
|
<application
|
||||||
<activity android:name="com.huawei.android.sdk.drm.DrmDialogActivity"
|
android:icon="@mipmap/icon_free"
|
||||||
android:configChanges="screenSize|orientation|keyboardHidden"
|
android:label="@string/app_name_free"
|
||||||
android:exported="false"
|
tools:replace="android:icon, android:label">
|
||||||
android:theme="@android:style/Theme.Translucent">
|
|
||||||
<meta-data
|
<activity
|
||||||
android:name="hwc-theme"
|
android:name="net.osmand.plus.activities.MapActivity"
|
||||||
android:value="androidhwext:style/Theme.Emui.Translucent" />
|
android:theme="@style/FirstSplashScreenFree"
|
||||||
</activity>
|
tools:replace="android:theme"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="net.osmand.plus.NavigationService"
|
||||||
|
tools:replace="android:process"
|
||||||
|
android:process="net.osmand.huawei"/>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="net.osmand.huawei.fileprovider"
|
tools:replace="android:authorities"
|
||||||
tools:replace="android:authorities" />
|
android:authorities="net.osmand.huawei.fileprovider"/>
|
||||||
<service
|
|
||||||
android:name="net.osmand.plus.NavigationService"
|
|
||||||
android:process="net.osmand.huawei"
|
|
||||||
tools:replace="android:process" />
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,25 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity android:name="com.huawei.android.sdk.drm.DrmDialogActivity"
|
|
||||||
android:configChanges="screenSize|orientation|keyboardHidden"
|
|
||||||
android:exported="false"
|
|
||||||
android:theme="@android:style/Theme.Translucent">
|
|
||||||
<meta-data
|
|
||||||
android:name="hwc-theme"
|
|
||||||
android:value="androidhwext:style/Theme.Emui.Translucent" />
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="net.osmand.plus.huawei.fileprovider"
|
|
||||||
tools:replace="android:authorities" />
|
|
||||||
<service
|
|
||||||
android:name="net.osmand.plus.NavigationService"
|
|
||||||
android:process="net.osmand.plus.huawei"
|
|
||||||
tools:replace="android:process" />
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
|
@ -40,10 +40,17 @@ android {
|
||||||
keyAlias "osmand"
|
keyAlias "osmand"
|
||||||
keyPassword System.getenv("OSMAND_APK_PASSWORD")
|
keyPassword System.getenv("OSMAND_APK_PASSWORD")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publishingHuawei {
|
||||||
|
storeFile file("/var/lib/jenkins/osmand_hw_key")
|
||||||
|
storePassword System.getenv("OSMAND_HW_APK_PASSWORD")
|
||||||
|
keyAlias "OsmAndHms"
|
||||||
|
keyPassword System.getenv("OSMAND_HW_APK_PASSWORD")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion System.getenv("MIN_SDK_VERSION") ? System.getenv("MIN_SDK_VERSION").toInteger() : 15
|
minSdkVersion System.getenv("MIN_SDK_VERSION") ? System.getenv("MIN_SDK_VERSION").toInteger() : 17
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 390
|
versionCode 390
|
||||||
versionCode System.getenv("APK_NUMBER_VERSION") ? System.getenv("APK_NUMBER_VERSION").toInteger() : versionCode
|
versionCode System.getenv("APK_NUMBER_VERSION") ? System.getenv("APK_NUMBER_VERSION").toInteger() : versionCode
|
||||||
|
@ -107,19 +114,23 @@ android {
|
||||||
debug {
|
debug {
|
||||||
manifest.srcFile "AndroidManifest-debug.xml"
|
manifest.srcFile "AndroidManifest-debug.xml"
|
||||||
}
|
}
|
||||||
|
full {
|
||||||
|
java.srcDirs = ["src-google"]
|
||||||
|
}
|
||||||
free {
|
free {
|
||||||
|
java.srcDirs = ["src-google"]
|
||||||
manifest.srcFile "AndroidManifest-free.xml"
|
manifest.srcFile "AndroidManifest-free.xml"
|
||||||
}
|
}
|
||||||
freedev {
|
freedev {
|
||||||
|
java.srcDirs = ["src-google"]
|
||||||
manifest.srcFile "AndroidManifest-freedev.xml"
|
manifest.srcFile "AndroidManifest-freedev.xml"
|
||||||
}
|
}
|
||||||
freecustom {
|
freecustom {
|
||||||
|
java.srcDirs = ["src-google"]
|
||||||
manifest.srcFile "AndroidManifest-freecustom.xml"
|
manifest.srcFile "AndroidManifest-freecustom.xml"
|
||||||
}
|
}
|
||||||
huawei {
|
|
||||||
manifest.srcFile "AndroidManifest-huawei.xml"
|
|
||||||
}
|
|
||||||
freehuawei {
|
freehuawei {
|
||||||
|
java.srcDirs = ["src-huawei"]
|
||||||
manifest.srcFile "AndroidManifest-freehuawei.xml"
|
manifest.srcFile "AndroidManifest-freehuawei.xml"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,10 +201,6 @@ android {
|
||||||
applicationId "net.osmand.plus"
|
applicationId "net.osmand.plus"
|
||||||
resConfig "en"
|
resConfig "en"
|
||||||
//resConfigs "xxhdpi", "nodpi"
|
//resConfigs "xxhdpi", "nodpi"
|
||||||
}
|
|
||||||
huawei {
|
|
||||||
dimension "version"
|
|
||||||
applicationId "net.osmand.plus.huawei"
|
|
||||||
}
|
}
|
||||||
freehuawei {
|
freehuawei {
|
||||||
dimension "version"
|
dimension "version"
|
||||||
|
@ -219,7 +226,10 @@ android {
|
||||||
signingConfig signingConfigs.development
|
signingConfig signingConfigs.development
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.publishing
|
productFlavors.all { flavor ->
|
||||||
|
flavor.signingConfig signingConfigs.publishing
|
||||||
|
}
|
||||||
|
productFlavors.freehuawei.signingConfig signingConfigs.publishingHuawei
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,46 +286,14 @@ task downloadWorldMiniBasemap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task downloadHuaweiDrmZip {
|
task setupHuaweiConfig {
|
||||||
doLast {
|
doLast {
|
||||||
ant.get(src: 'https://obs.cn-north-2.myhwclouds.com/hms-ds-wf/sdk/HwDRM_SDK_2.5.2.300_ADT.zip', dest: 'HwDRM_SDK_2.5.2.300_ADT.zip', skipexisting: 'true')
|
if (System.getenv("HUAWEI_SDK_JSON")) {
|
||||||
ant.unzip(src: 'HwDRM_SDK_2.5.2.300_ADT.zip', dest: 'huaweidrmlib/')
|
new File("agconnect-services.json").text = System.getenv("HUAWEI_SDK_JSON")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
task copyHuaweiDrmLibs(type: Copy) {
|
|
||||||
dependsOn downloadHuaweiDrmZip
|
|
||||||
from "huaweidrmlib/HwDRM_SDK_2.5.2.300_ADT/libs"
|
|
||||||
into "libs"
|
|
||||||
}
|
|
||||||
|
|
||||||
task copyHuaweiDrmValues(type: Copy) {
|
|
||||||
dependsOn downloadHuaweiDrmZip
|
|
||||||
from "huaweidrmlib/HwDRM_SDK_2.5.2.300_ADT/res"
|
|
||||||
into "res"
|
|
||||||
}
|
|
||||||
|
|
||||||
task downloadPrebuiltHuaweiDrm {
|
|
||||||
dependsOn copyHuaweiDrmLibs, copyHuaweiDrmValues
|
|
||||||
}
|
|
||||||
|
|
||||||
task cleanHuaweiDrmLibs(type: Delete) {
|
|
||||||
delete "huaweidrmlib"
|
|
||||||
delete fileTree("libs").matching {
|
|
||||||
include "**/huawei-*.jar"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task cleanHuaweiDrmValues(type: Delete) {
|
|
||||||
delete fileTree("res").matching {
|
|
||||||
include "**/drm_strings.xml"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task cleanPrebuiltHuaweiDrm {
|
|
||||||
dependsOn cleanHuaweiDrmLibs, cleanHuaweiDrmValues
|
|
||||||
}
|
|
||||||
|
|
||||||
task collectVoiceAssets(type: Sync) {
|
task collectVoiceAssets(type: Sync) {
|
||||||
from "../../resources/voice"
|
from "../../resources/voice"
|
||||||
into "assets/voice"
|
into "assets/voice"
|
||||||
|
@ -397,8 +375,6 @@ task copyLargePOIIcons(type: Sync) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
task copyWidgetIconsXhdpi(type: Sync) {
|
task copyWidgetIconsXhdpi(type: Sync) {
|
||||||
from "res/drawable-xxhdpi/"
|
from "res/drawable-xxhdpi/"
|
||||||
into "res/drawable-large-xhdpi/"
|
into "res/drawable-large-xhdpi/"
|
||||||
|
@ -445,13 +421,9 @@ task collectExternalResources {
|
||||||
copyPoiCategories,
|
copyPoiCategories,
|
||||||
downloadWorldMiniBasemap
|
downloadWorldMiniBasemap
|
||||||
|
|
||||||
Gradle gradle = getGradle()
|
String tskReqStr = gradle.startParameter.taskNames.toString()
|
||||||
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString().toLowerCase()
|
|
||||||
// Use Drm SDK only for huawei build
|
|
||||||
if (tskReqStr.contains("huawei")) {
|
if (tskReqStr.contains("huawei")) {
|
||||||
dependsOn downloadPrebuiltHuaweiDrm
|
dependsOn setupHuaweiConfig
|
||||||
} else {
|
|
||||||
dependsOn cleanPrebuiltHuaweiDrm
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -503,10 +475,16 @@ task cleanupDuplicatesInCore() {
|
||||||
file("libs/x86_64/libc++_shared.so").renameTo(file("libc++/x86_64/libc++_shared.so"))
|
file("libs/x86_64/libc++_shared.so").renameTo(file("libc++/x86_64/libc++_shared.so"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
android.applicationVariants.all { variant ->
|
android.applicationVariants.all { variant ->
|
||||||
variant.javaCompiler.dependsOn(collectExternalResources, buildOsmAndCore, cleanupDuplicatesInCore)
|
variant.javaCompiler.dependsOn(collectExternalResources, buildOsmAndCore, cleanupDuplicatesInCore)
|
||||||
}
|
}
|
||||||
|
Gradle gradle = getGradle()
|
||||||
|
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString().toLowerCase()
|
||||||
|
if (tskReqStr.contains("huawei")) {
|
||||||
|
apply plugin: 'com.huawei.agconnect'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task appStart(type: Exec) {
|
task appStart(type: Exec) {
|
||||||
|
@ -516,7 +494,6 @@ task appStart(type: Exec) {
|
||||||
// commandLine 'cmd', '/c', 'adb', 'shell', 'am', 'start', '-n', 'net.osmand.plus/net.osmand.plus.activities.MapActivity'
|
// commandLine 'cmd', '/c', 'adb', 'shell', 'am', 'start', '-n', 'net.osmand.plus/net.osmand.plus.activities.MapActivity'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(path: ':OsmAnd-java', configuration: 'android')
|
implementation project(path: ':OsmAnd-java', configuration: 'android')
|
||||||
implementation project(':OsmAnd-api')
|
implementation project(':OsmAnd-api')
|
||||||
|
@ -565,6 +542,6 @@ dependencies {
|
||||||
}
|
}
|
||||||
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
||||||
|
|
||||||
huaweiImplementation files('libs/huawei-android-drm_v2.5.2.300.jar')
|
//freehuaweiImplementation 'com.huawei.agconnect:agconnect-core:1.4.1.300'
|
||||||
freehuaweiImplementation files('libs/huawei-android-drm_v2.5.2.300.jar')
|
freehuaweiImplementation 'com.huawei.hms:iap:5.0.2.300'
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,546 @@
|
||||||
|
package net.osmand.plus.inapp;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.android.billingclient.api.BillingClient;
|
||||||
|
import com.android.billingclient.api.BillingResult;
|
||||||
|
import com.android.billingclient.api.Purchase;
|
||||||
|
import com.android.billingclient.api.SkuDetails;
|
||||||
|
import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||||
|
|
||||||
|
import net.osmand.AndroidUtils;
|
||||||
|
import net.osmand.plus.OsmandApplication;
|
||||||
|
import net.osmand.plus.inapp.util.BillingManager;
|
||||||
|
import net.osmand.plus.settings.backend.OsmandSettings;
|
||||||
|
import net.osmand.util.Algorithms;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class InAppPurchaseHelperImpl extends InAppPurchaseHelper {
|
||||||
|
|
||||||
|
// The helper object
|
||||||
|
private BillingManager billingManager;
|
||||||
|
private List<SkuDetails> skuDetailsList;
|
||||||
|
|
||||||
|
/* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY
|
||||||
|
* (that you got from the Google Play developer console). This is not your
|
||||||
|
* developer public key, it's the *app-specific* public key.
|
||||||
|
*
|
||||||
|
* Instead of just storing the entire literal string here embedded in the
|
||||||
|
* program, construct the key at runtime from pieces or
|
||||||
|
* use bit manipulation (for example, XOR with some other string) to hide
|
||||||
|
* the actual key. The key itself is not secret information, but we don't
|
||||||
|
* want to make it easy for an attacker to replace the public key with one
|
||||||
|
* of their own and then fake messages from the server.
|
||||||
|
*/
|
||||||
|
private static final String BASE64_ENCODED_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgk8cEx" +
|
||||||
|
"UO4mfEwWFLkQnX1Tkzehr4SnXLXcm2Osxs5FTJPEgyTckTh0POKVMrxeGLn0KoTY2NTgp1U/inp" +
|
||||||
|
"wccWisPhVPEmw9bAVvWsOkzlyg1kv03fJdnAXRBSqDDPV6X8Z3MtkPVqZkupBsxyIllEILKHK06" +
|
||||||
|
"OCw49JLTsMR3oTRifGzma79I71X0spw0fM+cIRlkS2tsXN8GPbdkJwHofZKPOXS51pgC1zU8uWX" +
|
||||||
|
"I+ftJO46a1XkNh1dO2anUiQ8P/H4yOTqnMsXF7biyYuiwjXPOcy0OMhEHi54Dq6Mr3u5ZALOAkc" +
|
||||||
|
"YTjh1H/ZgqIHy5ZluahINuDE76qdLYMXrDMQIDAQAB";
|
||||||
|
|
||||||
|
public InAppPurchaseHelperImpl(OsmandApplication ctx) {
|
||||||
|
super(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BillingManager getBillingManager() {
|
||||||
|
return billingManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppRunnable runnable) {
|
||||||
|
billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingManager.BillingUpdatesListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBillingClientSetupFinished() {
|
||||||
|
logDebug("Setup finished.");
|
||||||
|
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
// Have we been disposed of in the meantime? If so, quit.
|
||||||
|
if (billingManager == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!billingManager.isServiceConnected()) {
|
||||||
|
// Oh noes, there was a problem.
|
||||||
|
//complain("Problem setting up in-app billing: " + result);
|
||||||
|
notifyError(taskType, billingManager.getBillingClientResponseMessage());
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
processingTask = !runnable.run(InAppPurchaseHelperImpl.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConsumeFinished(String token, BillingResult billingResult) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPurchasesUpdated(final List<Purchase> purchases) {
|
||||||
|
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
// Have we been disposed of in the meantime? If so, quit.
|
||||||
|
if (billingManager == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTask == InAppPurchaseTaskType.REQUEST_INVENTORY) {
|
||||||
|
List<String> skuInApps = new ArrayList<>();
|
||||||
|
for (InAppPurchases.InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) {
|
||||||
|
skuInApps.add(purchase.getSku());
|
||||||
|
}
|
||||||
|
for (Purchase p : purchases) {
|
||||||
|
skuInApps.add(p.getSku());
|
||||||
|
}
|
||||||
|
billingManager.querySkuDetailsAsync(BillingClient.SkuType.INAPP, skuInApps, new SkuDetailsResponseListener() {
|
||||||
|
@Override
|
||||||
|
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListInApps) {
|
||||||
|
// Is it a failure?
|
||||||
|
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
|
||||||
|
logError("Failed to query inapps sku details: " + billingResult.getResponseCode());
|
||||||
|
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> skuSubscriptions = new ArrayList<>();
|
||||||
|
for (InAppPurchases.InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) {
|
||||||
|
skuSubscriptions.add(subscription.getSku());
|
||||||
|
}
|
||||||
|
for (Purchase p : purchases) {
|
||||||
|
skuSubscriptions.add(p.getSku());
|
||||||
|
}
|
||||||
|
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
// Have we been disposed of in the meantime? If so, quit.
|
||||||
|
if (billingManager == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
billingManager.querySkuDetailsAsync(BillingClient.SkuType.SUBS, skuSubscriptions, new SkuDetailsResponseListener() {
|
||||||
|
@Override
|
||||||
|
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListSubscriptions) {
|
||||||
|
// Is it a failure?
|
||||||
|
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
|
||||||
|
logError("Failed to query subscriptipons sku details: " + billingResult.getResponseCode());
|
||||||
|
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SkuDetails> skuDetailsList = new ArrayList<>(skuDetailsListInApps);
|
||||||
|
skuDetailsList.addAll(skuDetailsListSubscriptions);
|
||||||
|
InAppPurchaseHelperImpl.this.skuDetailsList = skuDetailsList;
|
||||||
|
|
||||||
|
mSkuDetailsResponseListener.onSkuDetailsResponse(billingResult, skuDetailsList);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (Purchase purchase : purchases) {
|
||||||
|
if (!purchase.isAcknowledged()) {
|
||||||
|
onPurchaseFinished(purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPurchaseCanceled() {
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void purchaseFullVersion(final Activity activity) {
|
||||||
|
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION);
|
||||||
|
exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, new InAppRunnable() {
|
||||||
|
@Override
|
||||||
|
public boolean run(InAppPurchaseHelper helper) {
|
||||||
|
try {
|
||||||
|
SkuDetails skuDetails = getSkuDetails(getFullVersion().getSku());
|
||||||
|
if (skuDetails == null) {
|
||||||
|
throw new IllegalArgumentException("Cannot find sku details");
|
||||||
|
}
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
billingManager.initiatePurchaseFlow(activity, skuDetails);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("BillingManager disposed");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
complain("Cannot launch full version purchase!");
|
||||||
|
logError("purchaseFullVersion Error", e);
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void purchaseDepthContours(final Activity activity) {
|
||||||
|
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS);
|
||||||
|
exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, new InAppRunnable() {
|
||||||
|
@Override
|
||||||
|
public boolean run(InAppPurchaseHelper helper) {
|
||||||
|
try {
|
||||||
|
SkuDetails skuDetails = getSkuDetails(getDepthContours().getSku());
|
||||||
|
if (skuDetails == null) {
|
||||||
|
throw new IllegalArgumentException("Cannot find sku details");
|
||||||
|
}
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
billingManager.initiatePurchaseFlow(activity, skuDetails);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("BillingManager disposed");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
complain("Cannot launch depth contours purchase!");
|
||||||
|
logError("purchaseDepthContours Error", e);
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private SkuDetails getSkuDetails(@NonNull String sku) {
|
||||||
|
List<SkuDetails> skuDetailsList = this.skuDetailsList;
|
||||||
|
if (skuDetailsList != null) {
|
||||||
|
for (SkuDetails details : skuDetailsList) {
|
||||||
|
if (details.getSku().equals(sku)) {
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasDetails(@NonNull String sku) {
|
||||||
|
return getSkuDetails(sku) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Purchase getPurchase(@NonNull String sku) {
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
List<Purchase> purchases = billingManager.getPurchases();
|
||||||
|
if (purchases != null) {
|
||||||
|
for (Purchase p : purchases) {
|
||||||
|
if (p.getSku().equals(sku)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listener that's called when we finish querying the items and subscriptions we own
|
||||||
|
private SkuDetailsResponseListener mSkuDetailsResponseListener = new SkuDetailsResponseListener() {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private List<String> getAllOwnedSubscriptionSkus() {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
for (Purchase p : billingManager.getPurchases()) {
|
||||||
|
if (getInAppPurchases().getInAppSubscriptionBySku(p.getSku()) != null) {
|
||||||
|
result.add(p.getSku());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
||||||
|
|
||||||
|
logDebug("Query sku details finished.");
|
||||||
|
|
||||||
|
// Have we been disposed of in the meantime? If so, quit.
|
||||||
|
if (getBillingManager() == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it a failure?
|
||||||
|
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
|
||||||
|
logError("Failed to query inventory: " + billingResult.getResponseCode());
|
||||||
|
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logDebug("Query sku details was successful.");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check for items we own. Notice that for each purchase, we check
|
||||||
|
* the developer payload to see if it's correct! See
|
||||||
|
* verifyDeveloperPayload().
|
||||||
|
*/
|
||||||
|
|
||||||
|
List<String> allOwnedSubscriptionSkus = getAllOwnedSubscriptionSkus();
|
||||||
|
for (InAppPurchases.InAppSubscription s : getLiveUpdates().getAllSubscriptions()) {
|
||||||
|
if (hasDetails(s.getSku())) {
|
||||||
|
Purchase purchase = getPurchase(s.getSku());
|
||||||
|
SkuDetails liveUpdatesDetails = getSkuDetails(s.getSku());
|
||||||
|
if (liveUpdatesDetails != null) {
|
||||||
|
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
|
||||||
|
}
|
||||||
|
allOwnedSubscriptionSkus.remove(s.getSku());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String sku : allOwnedSubscriptionSkus) {
|
||||||
|
Purchase purchase = getPurchase(sku);
|
||||||
|
SkuDetails liveUpdatesDetails = getSkuDetails(sku);
|
||||||
|
if (liveUpdatesDetails != null) {
|
||||||
|
InAppPurchases.InAppSubscription s = getLiveUpdates().upgradeSubscription(sku);
|
||||||
|
if (s == null) {
|
||||||
|
s = new InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails);
|
||||||
|
}
|
||||||
|
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InAppPurchases.InAppPurchase fullVersion = getFullVersion();
|
||||||
|
if (hasDetails(fullVersion.getSku())) {
|
||||||
|
Purchase purchase = getPurchase(fullVersion.getSku());
|
||||||
|
SkuDetails fullPriceDetails = getSkuDetails(fullVersion.getSku());
|
||||||
|
if (fullPriceDetails != null) {
|
||||||
|
fetchInAppPurchase(fullVersion, fullPriceDetails, purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InAppPurchases.InAppPurchase depthContours = getDepthContours();
|
||||||
|
if (hasDetails(depthContours.getSku())) {
|
||||||
|
Purchase purchase = getPurchase(depthContours.getSku());
|
||||||
|
SkuDetails depthContoursDetails = getSkuDetails(depthContours.getSku());
|
||||||
|
if (depthContoursDetails != null) {
|
||||||
|
fetchInAppPurchase(depthContours, depthContoursDetails, purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InAppPurchases.InAppPurchase contourLines = getContourLines();
|
||||||
|
if (hasDetails(contourLines.getSku())) {
|
||||||
|
Purchase purchase = getPurchase(contourLines.getSku());
|
||||||
|
SkuDetails contourLinesDetails = getSkuDetails(contourLines.getSku());
|
||||||
|
if (contourLinesDetails != null) {
|
||||||
|
fetchInAppPurchase(contourLines, contourLinesDetails, purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Purchase fullVersionPurchase = getPurchase(fullVersion.getSku());
|
||||||
|
boolean fullVersionPurchased = fullVersionPurchase != null;
|
||||||
|
if (fullVersionPurchased) {
|
||||||
|
ctx.getSettings().FULL_VERSION_PURCHASED.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Purchase depthContoursPurchase = getPurchase(depthContours.getSku());
|
||||||
|
boolean depthContoursPurchased = depthContoursPurchase != null;
|
||||||
|
if (depthContoursPurchased) {
|
||||||
|
ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have the live updates?
|
||||||
|
boolean subscribedToLiveUpdates = false;
|
||||||
|
List<Purchase> liveUpdatesPurchases = new ArrayList<>();
|
||||||
|
for (InAppPurchases.InAppPurchase p : getLiveUpdates().getAllSubscriptions()) {
|
||||||
|
Purchase purchase = getPurchase(p.getSku());
|
||||||
|
if (purchase != null) {
|
||||||
|
liveUpdatesPurchases.add(purchase);
|
||||||
|
if (!subscribedToLiveUpdates) {
|
||||||
|
subscribedToLiveUpdates = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OsmandSettings.OsmandPreference<Long> subscriptionCancelledTime = ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME;
|
||||||
|
if (!subscribedToLiveUpdates && ctx.getSettings().LIVE_UPDATES_PURCHASED.get()) {
|
||||||
|
if (subscriptionCancelledTime.get() == 0) {
|
||||||
|
subscriptionCancelledTime.set(System.currentTimeMillis());
|
||||||
|
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false);
|
||||||
|
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false);
|
||||||
|
} else if (System.currentTimeMillis() - subscriptionCancelledTime.get() > SUBSCRIPTION_HOLDING_TIME_MSEC) {
|
||||||
|
ctx.getSettings().LIVE_UPDATES_PURCHASED.set(false);
|
||||||
|
if (!isDepthContoursPurchased(ctx)) {
|
||||||
|
ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (subscribedToLiveUpdates) {
|
||||||
|
subscriptionCancelledTime.set(0L);
|
||||||
|
ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastValidationCheckTime = System.currentTimeMillis();
|
||||||
|
logDebug("User " + (subscribedToLiveUpdates ? "HAS" : "DOES NOT HAVE")
|
||||||
|
+ " live updates purchased.");
|
||||||
|
|
||||||
|
OsmandSettings settings = ctx.getSettings();
|
||||||
|
settings.INAPPS_READ.set(true);
|
||||||
|
|
||||||
|
List<Purchase> tokensToSend = new ArrayList<>();
|
||||||
|
if (liveUpdatesPurchases.size() > 0) {
|
||||||
|
List<String> tokensSent = Arrays.asList(settings.BILLING_PURCHASE_TOKENS_SENT.get().split(";"));
|
||||||
|
for (Purchase purchase : liveUpdatesPurchases) {
|
||||||
|
if ((Algorithms.isEmpty(settings.BILLING_USER_ID.get()) || Algorithms.isEmpty(settings.BILLING_USER_TOKEN.get()))
|
||||||
|
&& !Algorithms.isEmpty(purchase.getDeveloperPayload())) {
|
||||||
|
String payload = purchase.getDeveloperPayload();
|
||||||
|
if (!Algorithms.isEmpty(payload)) {
|
||||||
|
String[] arr = payload.split(" ");
|
||||||
|
if (arr.length > 0) {
|
||||||
|
settings.BILLING_USER_ID.set(arr[0]);
|
||||||
|
}
|
||||||
|
if (arr.length > 1) {
|
||||||
|
token = arr[1];
|
||||||
|
settings.BILLING_USER_TOKEN.set(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tokensSent.contains(purchase.getSku())) {
|
||||||
|
tokensToSend.add(purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<PurchaseInfo> purchaseInfoList = new ArrayList<>();
|
||||||
|
for (Purchase purchase : tokensToSend) {
|
||||||
|
purchaseInfoList.add(getPurchaseInfo(purchase));
|
||||||
|
}
|
||||||
|
onSkuDetailsResponseDone(purchaseInfoList);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private PurchaseInfo getPurchaseInfo(Purchase purchase) {
|
||||||
|
return new PurchaseInfo(purchase.getSku(), purchase.getOrderId(), purchase.getPurchaseToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchInAppPurchase(@NonNull InAppPurchases.InAppPurchase inAppPurchase, @NonNull SkuDetails skuDetails, @Nullable Purchase purchase) {
|
||||||
|
if (purchase != null) {
|
||||||
|
inAppPurchase.setPurchaseState(InAppPurchases.InAppPurchase.PurchaseState.PURCHASED);
|
||||||
|
inAppPurchase.setPurchaseTime(purchase.getPurchaseTime());
|
||||||
|
} else {
|
||||||
|
inAppPurchase.setPurchaseState(InAppPurchases.InAppPurchase.PurchaseState.NOT_PURCHASED);
|
||||||
|
}
|
||||||
|
inAppPurchase.setPrice(skuDetails.getPrice());
|
||||||
|
inAppPurchase.setPriceCurrencyCode(skuDetails.getPriceCurrencyCode());
|
||||||
|
if (skuDetails.getPriceAmountMicros() > 0) {
|
||||||
|
inAppPurchase.setPriceValue(skuDetails.getPriceAmountMicros() / 1000000d);
|
||||||
|
}
|
||||||
|
String subscriptionPeriod = skuDetails.getSubscriptionPeriod();
|
||||||
|
if (!Algorithms.isEmpty(subscriptionPeriod)) {
|
||||||
|
if (inAppPurchase instanceof InAppPurchases.InAppSubscription) {
|
||||||
|
try {
|
||||||
|
((InAppPurchases.InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
LOG.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inAppPurchase instanceof InAppPurchases.InAppSubscription) {
|
||||||
|
String introductoryPrice = skuDetails.getIntroductoryPrice();
|
||||||
|
String introductoryPricePeriod = skuDetails.getIntroductoryPricePeriod();
|
||||||
|
String introductoryPriceCycles = skuDetails.getIntroductoryPriceCycles();
|
||||||
|
long introductoryPriceAmountMicros = skuDetails.getIntroductoryPriceAmountMicros();
|
||||||
|
if (!Algorithms.isEmpty(introductoryPrice)) {
|
||||||
|
InAppPurchases.InAppSubscription s = (InAppPurchases.InAppSubscription) inAppPurchase;
|
||||||
|
try {
|
||||||
|
s.setIntroductoryInfo(new InAppPurchases.InAppSubscriptionIntroductoryInfo(s, introductoryPrice,
|
||||||
|
introductoryPriceAmountMicros, introductoryPricePeriod, introductoryPriceCycles));
|
||||||
|
} catch (ParseException e) {
|
||||||
|
LOG.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InAppRunnable getPurchaseLiveUpdatesCommand(final WeakReference<Activity> activity, final String sku, final String payload) {
|
||||||
|
return new InAppRunnable() {
|
||||||
|
@Override
|
||||||
|
public boolean run(InAppPurchaseHelper helper) {
|
||||||
|
try {
|
||||||
|
Activity a = activity.get();
|
||||||
|
SkuDetails skuDetails = getSkuDetails(sku);
|
||||||
|
if (AndroidUtils.isActivityNotDestroyed(a) && skuDetails != null) {
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
billingManager.setPayload(payload);
|
||||||
|
billingManager.initiatePurchaseFlow(a, skuDetails);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("BillingManager disposed");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logError("launchPurchaseFlow Error", e);
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InAppRunnable getRequestInventoryCommand() {
|
||||||
|
return new InAppRunnable() {
|
||||||
|
@Override
|
||||||
|
public boolean run(InAppPurchaseHelper helper) {
|
||||||
|
logDebug("Setup successful. Querying inventory.");
|
||||||
|
try {
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
billingManager.queryPurchases();
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("BillingManager disposed");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logError("queryInventoryAsync Error", e);
|
||||||
|
notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY);
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call when a purchase is finished
|
||||||
|
private void onPurchaseFinished(Purchase purchase) {
|
||||||
|
logDebug("Purchase finished: " + purchase);
|
||||||
|
|
||||||
|
// if we were disposed of in the meantime, quit.
|
||||||
|
if (getBillingManager() == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPurchaseDone(getPurchaseInfo(purchase));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isBillingManagerExists() {
|
||||||
|
return getBillingManager() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void destroyBillingManager() {
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
billingManager.destroy();
|
||||||
|
this.billingManager = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,475 @@
|
||||||
|
package net.osmand.plus.inapp;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import net.osmand.AndroidUtils;
|
||||||
|
import net.osmand.plus.OsmandApplication;
|
||||||
|
import net.osmand.plus.inapp.util.BillingManager;
|
||||||
|
import net.osmand.plus.settings.backend.OsmandSettings;
|
||||||
|
import net.osmand.util.Algorithms;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class InAppPurchaseHelperImpl extends InAppPurchaseHelper {
|
||||||
|
|
||||||
|
// The helper object
|
||||||
|
private BillingManager billingManager;
|
||||||
|
private List<SkuDetails> skuDetailsList;
|
||||||
|
|
||||||
|
|
||||||
|
public InAppPurchaseHelperImpl(OsmandApplication ctx) {
|
||||||
|
super(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppRunnable runnable) {
|
||||||
|
billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingManager.BillingUpdatesListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBillingClientSetupFinished() {
|
||||||
|
logDebug("Setup finished.");
|
||||||
|
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
// Have we been disposed of in the meantime? If so, quit.
|
||||||
|
if (billingManager == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!billingManager.isServiceConnected()) {
|
||||||
|
// Oh noes, there was a problem.
|
||||||
|
//complain("Problem setting up in-app billing: " + result);
|
||||||
|
notifyError(taskType, billingManager.getBillingClientResponseMessage());
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
processingTask = !runnable.run(InAppPurchaseHelperImpl.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConsumeFinished(String token, BillingResult billingResult) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPurchasesUpdated(final List<Purchase> purchases) {
|
||||||
|
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
// Have we been disposed of in the meantime? If so, quit.
|
||||||
|
if (billingManager == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTask == InAppPurchaseTaskType.REQUEST_INVENTORY) {
|
||||||
|
List<String> skuInApps = new ArrayList<>();
|
||||||
|
for (InAppPurchases.InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) {
|
||||||
|
skuInApps.add(purchase.getSku());
|
||||||
|
}
|
||||||
|
for (Purchase p : purchases) {
|
||||||
|
skuInApps.add(p.getSku());
|
||||||
|
}
|
||||||
|
billingManager.querySkuDetailsAsync(BillingClient.SkuType.INAPP, skuInApps, new SkuDetailsResponseListener() {
|
||||||
|
@Override
|
||||||
|
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListInApps) {
|
||||||
|
// Is it a failure?
|
||||||
|
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
|
||||||
|
logError("Failed to query inapps sku details: " + billingResult.getResponseCode());
|
||||||
|
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> skuSubscriptions = new ArrayList<>();
|
||||||
|
for (InAppPurchases.InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) {
|
||||||
|
skuSubscriptions.add(subscription.getSku());
|
||||||
|
}
|
||||||
|
for (Purchase p : purchases) {
|
||||||
|
skuSubscriptions.add(p.getSku());
|
||||||
|
}
|
||||||
|
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
// Have we been disposed of in the meantime? If so, quit.
|
||||||
|
if (billingManager == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
billingManager.querySkuDetailsAsync(BillingClient.SkuType.SUBS, skuSubscriptions, new SkuDetailsResponseListener() {
|
||||||
|
@Override
|
||||||
|
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListSubscriptions) {
|
||||||
|
// Is it a failure?
|
||||||
|
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
|
||||||
|
logError("Failed to query subscriptipons sku details: " + billingResult.getResponseCode());
|
||||||
|
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SkuDetails> skuDetailsList = new ArrayList<>(skuDetailsListInApps);
|
||||||
|
skuDetailsList.addAll(skuDetailsListSubscriptions);
|
||||||
|
InAppPurchaseHelperImpl.this.skuDetailsList = skuDetailsList;
|
||||||
|
|
||||||
|
mSkuDetailsResponseListener.onSkuDetailsResponse(billingResult, skuDetailsList);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (Purchase purchase : purchases) {
|
||||||
|
if (!purchase.isAcknowledged()) {
|
||||||
|
onPurchaseFinished(purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPurchaseCanceled() {
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void purchaseFullVersion(Activity activity) throws UnsupportedOperationException {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void purchaseDepthContours(Activity activity) throws UnsupportedOperationException {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private SkuDetails getSkuDetails(@NonNull String sku) {
|
||||||
|
List<SkuDetails> skuDetailsList = this.skuDetailsList;
|
||||||
|
if (skuDetailsList != null) {
|
||||||
|
for (SkuDetails details : skuDetailsList) {
|
||||||
|
if (details.getSku().equals(sku)) {
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasDetails(@NonNull String sku) {
|
||||||
|
return getSkuDetails(sku) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Purchase getPurchase(@NonNull String sku) {
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
List<Purchase> purchases = billingManager.getPurchases();
|
||||||
|
if (purchases != null) {
|
||||||
|
for (Purchase p : purchases) {
|
||||||
|
if (p.getSku().equals(sku)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listener that's called when we finish querying the items and subscriptions we own
|
||||||
|
private SkuDetailsResponseListener mSkuDetailsResponseListener = new SkuDetailsResponseListener() {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private List<String> getAllOwnedSubscriptionSkus() {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
for (Purchase p : billingManager.getPurchases()) {
|
||||||
|
if (getInAppPurchases().getInAppSubscriptionBySku(p.getSku()) != null) {
|
||||||
|
result.add(p.getSku());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
||||||
|
|
||||||
|
logDebug("Query sku details finished.");
|
||||||
|
|
||||||
|
// Have we been disposed of in the meantime? If so, quit.
|
||||||
|
if (getBillingManager() == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it a failure?
|
||||||
|
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
|
||||||
|
logError("Failed to query inventory: " + billingResult.getResponseCode());
|
||||||
|
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logDebug("Query sku details was successful.");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check for items we own. Notice that for each purchase, we check
|
||||||
|
* the developer payload to see if it's correct! See
|
||||||
|
* verifyDeveloperPayload().
|
||||||
|
*/
|
||||||
|
|
||||||
|
List<String> allOwnedSubscriptionSkus = getAllOwnedSubscriptionSkus();
|
||||||
|
for (InAppPurchases.InAppSubscription s : getLiveUpdates().getAllSubscriptions()) {
|
||||||
|
if (hasDetails(s.getSku())) {
|
||||||
|
Purchase purchase = getPurchase(s.getSku());
|
||||||
|
SkuDetails liveUpdatesDetails = getSkuDetails(s.getSku());
|
||||||
|
if (liveUpdatesDetails != null) {
|
||||||
|
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
|
||||||
|
}
|
||||||
|
allOwnedSubscriptionSkus.remove(s.getSku());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String sku : allOwnedSubscriptionSkus) {
|
||||||
|
Purchase purchase = getPurchase(sku);
|
||||||
|
SkuDetails liveUpdatesDetails = getSkuDetails(sku);
|
||||||
|
if (liveUpdatesDetails != null) {
|
||||||
|
InAppPurchases.InAppSubscription s = getLiveUpdates().upgradeSubscription(sku);
|
||||||
|
if (s == null) {
|
||||||
|
s = new InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails);
|
||||||
|
}
|
||||||
|
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InAppPurchases.InAppPurchase fullVersion = getFullVersion();
|
||||||
|
if (hasDetails(fullVersion.getSku())) {
|
||||||
|
Purchase purchase = getPurchase(fullVersion.getSku());
|
||||||
|
SkuDetails fullPriceDetails = getSkuDetails(fullVersion.getSku());
|
||||||
|
if (fullPriceDetails != null) {
|
||||||
|
fetchInAppPurchase(fullVersion, fullPriceDetails, purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InAppPurchases.InAppPurchase depthContours = getDepthContours();
|
||||||
|
if (hasDetails(depthContours.getSku())) {
|
||||||
|
Purchase purchase = getPurchase(depthContours.getSku());
|
||||||
|
SkuDetails depthContoursDetails = getSkuDetails(depthContours.getSku());
|
||||||
|
if (depthContoursDetails != null) {
|
||||||
|
fetchInAppPurchase(depthContours, depthContoursDetails, purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InAppPurchases.InAppPurchase contourLines = getContourLines();
|
||||||
|
if (hasDetails(contourLines.getSku())) {
|
||||||
|
Purchase purchase = getPurchase(contourLines.getSku());
|
||||||
|
SkuDetails contourLinesDetails = getSkuDetails(contourLines.getSku());
|
||||||
|
if (contourLinesDetails != null) {
|
||||||
|
fetchInAppPurchase(contourLines, contourLinesDetails, purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Purchase fullVersionPurchase = getPurchase(fullVersion.getSku());
|
||||||
|
boolean fullVersionPurchased = fullVersionPurchase != null;
|
||||||
|
if (fullVersionPurchased) {
|
||||||
|
ctx.getSettings().FULL_VERSION_PURCHASED.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Purchase depthContoursPurchase = getPurchase(depthContours.getSku());
|
||||||
|
boolean depthContoursPurchased = depthContoursPurchase != null;
|
||||||
|
if (depthContoursPurchased) {
|
||||||
|
ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have the live updates?
|
||||||
|
boolean subscribedToLiveUpdates = false;
|
||||||
|
List<Purchase> liveUpdatesPurchases = new ArrayList<>();
|
||||||
|
for (InAppPurchases.InAppPurchase p : getLiveUpdates().getAllSubscriptions()) {
|
||||||
|
Purchase purchase = getPurchase(p.getSku());
|
||||||
|
if (purchase != null) {
|
||||||
|
liveUpdatesPurchases.add(purchase);
|
||||||
|
if (!subscribedToLiveUpdates) {
|
||||||
|
subscribedToLiveUpdates = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OsmandSettings.OsmandPreference<Long> subscriptionCancelledTime = ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME;
|
||||||
|
if (!subscribedToLiveUpdates && ctx.getSettings().LIVE_UPDATES_PURCHASED.get()) {
|
||||||
|
if (subscriptionCancelledTime.get() == 0) {
|
||||||
|
subscriptionCancelledTime.set(System.currentTimeMillis());
|
||||||
|
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false);
|
||||||
|
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false);
|
||||||
|
} else if (System.currentTimeMillis() - subscriptionCancelledTime.get() > SUBSCRIPTION_HOLDING_TIME_MSEC) {
|
||||||
|
ctx.getSettings().LIVE_UPDATES_PURCHASED.set(false);
|
||||||
|
if (!isDepthContoursPurchased(ctx)) {
|
||||||
|
ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (subscribedToLiveUpdates) {
|
||||||
|
subscriptionCancelledTime.set(0L);
|
||||||
|
ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastValidationCheckTime = System.currentTimeMillis();
|
||||||
|
logDebug("User " + (subscribedToLiveUpdates ? "HAS" : "DOES NOT HAVE")
|
||||||
|
+ " live updates purchased.");
|
||||||
|
|
||||||
|
OsmandSettings settings = ctx.getSettings();
|
||||||
|
settings.INAPPS_READ.set(true);
|
||||||
|
|
||||||
|
List<Purchase> tokensToSend = new ArrayList<>();
|
||||||
|
if (liveUpdatesPurchases.size() > 0) {
|
||||||
|
List<String> tokensSent = Arrays.asList(settings.BILLING_PURCHASE_TOKENS_SENT.get().split(";"));
|
||||||
|
for (Purchase purchase : liveUpdatesPurchases) {
|
||||||
|
if ((Algorithms.isEmpty(settings.BILLING_USER_ID.get()) || Algorithms.isEmpty(settings.BILLING_USER_TOKEN.get()))
|
||||||
|
&& !Algorithms.isEmpty(purchase.getDeveloperPayload())) {
|
||||||
|
String payload = purchase.getDeveloperPayload();
|
||||||
|
if (!Algorithms.isEmpty(payload)) {
|
||||||
|
String[] arr = payload.split(" ");
|
||||||
|
if (arr.length > 0) {
|
||||||
|
settings.BILLING_USER_ID.set(arr[0]);
|
||||||
|
}
|
||||||
|
if (arr.length > 1) {
|
||||||
|
token = arr[1];
|
||||||
|
settings.BILLING_USER_TOKEN.set(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tokensSent.contains(purchase.getSku())) {
|
||||||
|
tokensToSend.add(purchase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<PurchaseInfo> purchaseInfoList = new ArrayList<>();
|
||||||
|
for (Purchase purchase : tokensToSend) {
|
||||||
|
purchaseInfoList.add(getPurchaseInfo(purchase));
|
||||||
|
}
|
||||||
|
onSkuDetailsResponseDone(purchaseInfoList);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private PurchaseInfo getPurchaseInfo(Purchase purchase) {
|
||||||
|
return new PurchaseInfo(purchase.getSku(), purchase.getOrderId(), purchase.getPurchaseToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchInAppPurchase(@NonNull InAppPurchases.InAppPurchase inAppPurchase, @NonNull SkuDetails skuDetails, @Nullable Purchase purchase) {
|
||||||
|
if (purchase != null) {
|
||||||
|
inAppPurchase.setPurchaseState(InAppPurchases.InAppPurchase.PurchaseState.PURCHASED);
|
||||||
|
inAppPurchase.setPurchaseTime(purchase.getPurchaseTime());
|
||||||
|
} else {
|
||||||
|
inAppPurchase.setPurchaseState(InAppPurchases.InAppPurchase.PurchaseState.NOT_PURCHASED);
|
||||||
|
}
|
||||||
|
inAppPurchase.setPrice(skuDetails.getPrice());
|
||||||
|
inAppPurchase.setPriceCurrencyCode(skuDetails.getPriceCurrencyCode());
|
||||||
|
if (skuDetails.getPriceAmountMicros() > 0) {
|
||||||
|
inAppPurchase.setPriceValue(skuDetails.getPriceAmountMicros() / 1000000d);
|
||||||
|
}
|
||||||
|
String subscriptionPeriod = skuDetails.getSubscriptionPeriod();
|
||||||
|
if (!Algorithms.isEmpty(subscriptionPeriod)) {
|
||||||
|
if (inAppPurchase instanceof InAppPurchases.InAppSubscription) {
|
||||||
|
try {
|
||||||
|
((InAppPurchases.InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
LOG.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inAppPurchase instanceof InAppPurchases.InAppSubscription) {
|
||||||
|
String introductoryPrice = skuDetails.getIntroductoryPrice();
|
||||||
|
String introductoryPricePeriod = skuDetails.getIntroductoryPricePeriod();
|
||||||
|
String introductoryPriceCycles = skuDetails.getIntroductoryPriceCycles();
|
||||||
|
long introductoryPriceAmountMicros = skuDetails.getIntroductoryPriceAmountMicros();
|
||||||
|
if (!Algorithms.isEmpty(introductoryPrice)) {
|
||||||
|
InAppPurchases.InAppSubscription s = (InAppPurchases.InAppSubscription) inAppPurchase;
|
||||||
|
try {
|
||||||
|
s.setIntroductoryInfo(new InAppPurchases.InAppSubscriptionIntroductoryInfo(s, introductoryPrice,
|
||||||
|
introductoryPriceAmountMicros, introductoryPricePeriod, introductoryPriceCycles));
|
||||||
|
} catch (ParseException e) {
|
||||||
|
LOG.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InAppRunnable getPurchaseLiveUpdatesCommand(final WeakReference<Activity> activity, final String sku, final String payload) {
|
||||||
|
return new InAppRunnable() {
|
||||||
|
@Override
|
||||||
|
public boolean run(InAppPurchaseHelper helper) {
|
||||||
|
try {
|
||||||
|
Activity a = activity.get();
|
||||||
|
SkuDetails skuDetails = getSkuDetails(sku);
|
||||||
|
if (AndroidUtils.isActivityNotDestroyed(a) && skuDetails != null) {
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
billingManager.setPayload(payload);
|
||||||
|
billingManager.initiatePurchaseFlow(a, skuDetails);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("BillingManager disposed");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logError("launchPurchaseFlow Error", e);
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InAppRunnable getRequestInventoryCommand() {
|
||||||
|
return new InAppRunnable() {
|
||||||
|
@Override
|
||||||
|
public boolean run(InAppPurchaseHelper helper) {
|
||||||
|
logDebug("Setup successful. Querying inventory.");
|
||||||
|
try {
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
billingManager.queryPurchases();
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("BillingManager disposed");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logError("queryInventoryAsync Error", e);
|
||||||
|
notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY);
|
||||||
|
stop(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call when a purchase is finished
|
||||||
|
private void onPurchaseFinished(Purchase purchase) {
|
||||||
|
logDebug("Purchase finished: " + purchase);
|
||||||
|
|
||||||
|
// if we were disposed of in the meantime, quit.
|
||||||
|
if (getBillingManager() == null) {
|
||||||
|
stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPurchaseDone(getPurchaseInfo(purchase));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isBillingManagerExists() {
|
||||||
|
return getBillingManager() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void destroyBillingManager() {
|
||||||
|
BillingManager billingManager = getBillingManager();
|
||||||
|
if (billingManager != null) {
|
||||||
|
billingManager.destroy();
|
||||||
|
this.billingManager = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ import net.osmand.plus.download.ui.AbstractLoadLocalIndexTask;
|
||||||
import net.osmand.plus.helpers.AvoidSpecificRoads;
|
import net.osmand.plus.helpers.AvoidSpecificRoads;
|
||||||
import net.osmand.plus.helpers.LockHelper;
|
import net.osmand.plus.helpers.LockHelper;
|
||||||
import net.osmand.plus.helpers.WaypointHelper;
|
import net.osmand.plus.helpers.WaypointHelper;
|
||||||
import net.osmand.plus.inapp.InAppPurchaseHelper;
|
import net.osmand.plus.inapp.InAppPurchaseHelperImpl;
|
||||||
import net.osmand.plus.liveupdates.LiveUpdatesHelper;
|
import net.osmand.plus.liveupdates.LiveUpdatesHelper;
|
||||||
import net.osmand.plus.mapmarkers.MapMarkersDbHelper;
|
import net.osmand.plus.mapmarkers.MapMarkersDbHelper;
|
||||||
import net.osmand.plus.monitoring.LiveMonitoringHelper;
|
import net.osmand.plus.monitoring.LiveMonitoringHelper;
|
||||||
|
@ -428,7 +428,7 @@ public class AppInitializer implements IProgress {
|
||||||
}
|
}
|
||||||
getLazyRoutingConfig();
|
getLazyRoutingConfig();
|
||||||
app.applyTheme(app);
|
app.applyTheme(app);
|
||||||
app.inAppPurchaseHelper = startupInit(new InAppPurchaseHelper(app), InAppPurchaseHelper.class);
|
app.inAppPurchaseHelper = startupInit(new InAppPurchaseHelperImpl(app), InAppPurchaseHelperImpl.class);
|
||||||
app.poiTypes = startupInit(MapPoiTypes.getDefaultNoInit(), MapPoiTypes.class);
|
app.poiTypes = startupInit(MapPoiTypes.getDefaultNoInit(), MapPoiTypes.class);
|
||||||
app.transportRoutingHelper = startupInit(new TransportRoutingHelper(app), TransportRoutingHelper.class);
|
app.transportRoutingHelper = startupInit(new TransportRoutingHelper(app), TransportRoutingHelper.class);
|
||||||
app.routingHelper = startupInit(new RoutingHelper(app), RoutingHelper.class);
|
app.routingHelper = startupInit(new RoutingHelper(app), RoutingHelper.class);
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
package net.osmand.plus;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public class HuaweiDrmHelper {
|
|
||||||
private static final String TAG = HuaweiDrmHelper.class.getSimpleName();
|
|
||||||
|
|
||||||
//Copyright protection id
|
|
||||||
private static final String DRM_ID = "101117397";
|
|
||||||
//Copyright protection public key
|
|
||||||
private static final String DRM_PUBLIC_KEY = "9d6f861e7d46be167809a6a62302749a6753b3c1bd02c9729efb3973e268091d";
|
|
||||||
|
|
||||||
public static void check(Activity activity) {
|
|
||||||
boolean succeed = false;
|
|
||||||
try {
|
|
||||||
final WeakReference<Activity> activityRef = new WeakReference<>(activity);
|
|
||||||
Class<?> drmCheckCallbackClass = Class.forName("com.huawei.android.sdk.drm.DrmCheckCallback");
|
|
||||||
Object callback = java.lang.reflect.Proxy.newProxyInstance(
|
|
||||||
drmCheckCallbackClass.getClassLoader(),
|
|
||||||
new java.lang.Class[]{drmCheckCallbackClass},
|
|
||||||
new java.lang.reflect.InvocationHandler() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) {
|
|
||||||
Activity a = activityRef.get();
|
|
||||||
if (a != null && !a.isFinishing()) {
|
|
||||||
String method_name = method.getName();
|
|
||||||
if (method_name.equals("onCheckSuccess")) {
|
|
||||||
// skip now
|
|
||||||
} else if (method_name.equals("onCheckFailed")) {
|
|
||||||
closeApplication(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Class<?> drmClass = Class.forName("com.huawei.android.sdk.drm.Drm");
|
|
||||||
Class[] partypes = new Class[]{Activity.class, String.class, String.class, String.class, drmCheckCallbackClass};
|
|
||||||
Method check = drmClass.getMethod("check", partypes);
|
|
||||||
check.invoke(null, activity, activity.getPackageName(), DRM_ID, DRM_PUBLIC_KEY, callback);
|
|
||||||
succeed = true;
|
|
||||||
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
Log.e(TAG, "check: ", e);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Log.e(TAG, "check: ", e);
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
Log.e(TAG, "check: ", e);
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
Log.e(TAG, "check: ", e);
|
|
||||||
}
|
|
||||||
if (!succeed) {
|
|
||||||
closeApplication(activity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void closeApplication(Activity activity) {
|
|
||||||
((OsmandApplication) activity.getApplication()).closeApplicationAnywayImpl(activity, true);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -121,8 +121,8 @@ public class Version {
|
||||||
public static boolean isFreeVersion(OsmandApplication ctx){
|
public static boolean isFreeVersion(OsmandApplication ctx){
|
||||||
return ctx.getPackageName().equals(FREE_VERSION_NAME) ||
|
return ctx.getPackageName().equals(FREE_VERSION_NAME) ||
|
||||||
ctx.getPackageName().equals(FREE_DEV_VERSION_NAME) ||
|
ctx.getPackageName().equals(FREE_DEV_VERSION_NAME) ||
|
||||||
ctx.getPackageName().equals(FREE_CUSTOM_VERSION_NAME)
|
ctx.getPackageName().equals(FREE_CUSTOM_VERSION_NAME) ||
|
||||||
;
|
isHuawei(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isPaidVersion(OsmandApplication ctx) {
|
public static boolean isPaidVersion(OsmandApplication ctx) {
|
||||||
|
|
|
@ -67,7 +67,6 @@ import net.osmand.plus.AppInitializer;
|
||||||
import net.osmand.plus.AppInitializer.AppInitializeListener;
|
import net.osmand.plus.AppInitializer.AppInitializeListener;
|
||||||
import net.osmand.plus.AppInitializer.InitEvents;
|
import net.osmand.plus.AppInitializer.InitEvents;
|
||||||
import net.osmand.plus.GpxSelectionHelper.GpxDisplayItem;
|
import net.osmand.plus.GpxSelectionHelper.GpxDisplayItem;
|
||||||
import net.osmand.plus.HuaweiDrmHelper;
|
|
||||||
import net.osmand.plus.MapMarkersHelper.MapMarker;
|
import net.osmand.plus.MapMarkersHelper.MapMarker;
|
||||||
import net.osmand.plus.MapMarkersHelper.MapMarkerChangedListener;
|
import net.osmand.plus.MapMarkersHelper.MapMarkerChangedListener;
|
||||||
import net.osmand.plus.OnDismissDialogFragmentListener;
|
import net.osmand.plus.OnDismissDialogFragmentListener;
|
||||||
|
@ -276,9 +275,6 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if (Version.isHuawei(getMyApplication())) {
|
|
||||||
HuaweiDrmHelper.check(this);
|
|
||||||
}
|
|
||||||
// Full screen is not used here
|
// Full screen is not used here
|
||||||
// getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
// getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
setContentView(R.layout.main);
|
setContentView(R.layout.main);
|
||||||
|
|
|
@ -81,7 +81,7 @@ public class DiscountHelper {
|
||||||
public static void checkAndDisplay(final MapActivity mapActivity) {
|
public static void checkAndDisplay(final MapActivity mapActivity) {
|
||||||
OsmandApplication app = mapActivity.getMyApplication();
|
OsmandApplication app = mapActivity.getMyApplication();
|
||||||
OsmandSettings settings = app.getSettings();
|
OsmandSettings settings = app.getSettings();
|
||||||
if (settings.DO_NOT_SHOW_STARTUP_MESSAGES.get() || !settings.INAPPS_READ.get() || Version.isHuawei(app)) {
|
if (settings.DO_NOT_SHOW_STARTUP_MESSAGES.get() || !settings.INAPPS_READ.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mBannerVisible) {
|
if (mBannerVisible) {
|
||||||
|
|
|
@ -9,33 +9,21 @@ import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.android.billingclient.api.BillingClient.BillingResponseCode;
|
|
||||||
import com.android.billingclient.api.BillingClient.SkuType;
|
|
||||||
import com.android.billingclient.api.BillingResult;
|
|
||||||
import com.android.billingclient.api.Purchase;
|
|
||||||
import com.android.billingclient.api.SkuDetails;
|
|
||||||
import com.android.billingclient.api.SkuDetailsResponseListener;
|
|
||||||
|
|
||||||
import net.osmand.AndroidNetworkUtils;
|
import net.osmand.AndroidNetworkUtils;
|
||||||
import net.osmand.AndroidNetworkUtils.OnRequestResultListener;
|
import net.osmand.AndroidNetworkUtils.OnRequestResultListener;
|
||||||
import net.osmand.AndroidNetworkUtils.OnRequestsResultListener;
|
import net.osmand.AndroidNetworkUtils.OnRequestsResultListener;
|
||||||
import net.osmand.AndroidNetworkUtils.RequestResponse;
|
import net.osmand.AndroidNetworkUtils.RequestResponse;
|
||||||
import net.osmand.PlatformUtil;
|
import net.osmand.PlatformUtil;
|
||||||
import net.osmand.plus.OsmandApplication;
|
import net.osmand.plus.OsmandApplication;
|
||||||
import net.osmand.plus.settings.backend.OsmandSettings;
|
|
||||||
import net.osmand.plus.settings.backend.OsmandSettings.OsmandPreference;
|
|
||||||
import net.osmand.plus.R;
|
import net.osmand.plus.R;
|
||||||
import net.osmand.plus.Version;
|
import net.osmand.plus.Version;
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppPurchase;
|
import net.osmand.plus.inapp.InAppPurchases.InAppPurchase;
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppPurchase.PurchaseState;
|
import net.osmand.plus.inapp.InAppPurchases.InAppPurchase.PurchaseState;
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription;
|
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppSubscription;
|
import net.osmand.plus.inapp.InAppPurchases.InAppSubscription;
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionIntroductoryInfo;
|
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionList;
|
import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionList;
|
||||||
import net.osmand.plus.inapp.util.BillingManager;
|
|
||||||
import net.osmand.plus.inapp.util.BillingManager.BillingUpdatesListener;
|
|
||||||
import net.osmand.plus.liveupdates.CountrySelectionFragment;
|
import net.osmand.plus.liveupdates.CountrySelectionFragment;
|
||||||
import net.osmand.plus.liveupdates.CountrySelectionFragment.CountryItem;
|
import net.osmand.plus.liveupdates.CountrySelectionFragment.CountryItem;
|
||||||
|
import net.osmand.plus.settings.backend.OsmandSettings;
|
||||||
import net.osmand.util.Algorithms;
|
import net.osmand.util.Algorithms;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
|
@ -43,7 +31,6 @@ import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.text.ParseException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -53,52 +40,28 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class InAppPurchaseHelper {
|
public abstract class InAppPurchaseHelper {
|
||||||
// Debug tag, for logging
|
// Debug tag, for logging
|
||||||
private static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(InAppPurchaseHelper.class);
|
protected static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(InAppPurchaseHelper.class);
|
||||||
private static final String TAG = InAppPurchaseHelper.class.getSimpleName();
|
private static final String TAG = InAppPurchaseHelper.class.getSimpleName();
|
||||||
private boolean mDebugLog = false;
|
private boolean mDebugLog = false;
|
||||||
|
|
||||||
public static final long SUBSCRIPTION_HOLDING_TIME_MSEC = 1000 * 60 * 60 * 24 * 3; // 3 days
|
public static final long SUBSCRIPTION_HOLDING_TIME_MSEC = 1000 * 60 * 60 * 24 * 3; // 3 days
|
||||||
|
|
||||||
private InAppPurchases purchases;
|
protected InAppPurchases purchases;
|
||||||
private long lastValidationCheckTime;
|
protected long lastValidationCheckTime;
|
||||||
private boolean inventoryRequested;
|
protected boolean inventoryRequested;
|
||||||
|
|
||||||
private static final long PURCHASE_VALIDATION_PERIOD_MSEC = 1000 * 60 * 60 * 24; // daily
|
private static final long PURCHASE_VALIDATION_PERIOD_MSEC = 1000 * 60 * 60 * 24; // daily
|
||||||
// (arbitrary) request code for the purchase flow
|
|
||||||
private static final int RC_REQUEST = 10001;
|
|
||||||
|
|
||||||
// The helper object
|
protected boolean isDeveloperVersion;
|
||||||
private BillingManager billingManager;
|
protected String token = "";
|
||||||
private List<SkuDetails> skuDetailsList;
|
protected InAppPurchaseTaskType activeTask;
|
||||||
|
protected boolean processingTask = false;
|
||||||
|
protected boolean inventoryRequestPending = false;
|
||||||
|
|
||||||
private boolean isDeveloperVersion;
|
protected OsmandApplication ctx;
|
||||||
private String token = "";
|
protected InAppPurchaseListener uiActivity = null;
|
||||||
private InAppPurchaseTaskType activeTask;
|
|
||||||
private boolean processingTask = false;
|
|
||||||
private boolean inventoryRequestPending = false;
|
|
||||||
|
|
||||||
private OsmandApplication ctx;
|
|
||||||
private InAppPurchaseListener uiActivity = null;
|
|
||||||
|
|
||||||
/* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY
|
|
||||||
* (that you got from the Google Play developer console). This is not your
|
|
||||||
* developer public key, it's the *app-specific* public key.
|
|
||||||
*
|
|
||||||
* Instead of just storing the entire literal string here embedded in the
|
|
||||||
* program, construct the key at runtime from pieces or
|
|
||||||
* use bit manipulation (for example, XOR with some other string) to hide
|
|
||||||
* the actual key. The key itself is not secret information, but we don't
|
|
||||||
* want to make it easy for an attacker to replace the public key with one
|
|
||||||
* of their own and then fake messages from the server.
|
|
||||||
*/
|
|
||||||
private static final String BASE64_ENCODED_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgk8cEx" +
|
|
||||||
"UO4mfEwWFLkQnX1Tkzehr4SnXLXcm2Osxs5FTJPEgyTckTh0POKVMrxeGLn0KoTY2NTgp1U/inp" +
|
|
||||||
"wccWisPhVPEmw9bAVvWsOkzlyg1kv03fJdnAXRBSqDDPV6X8Z3MtkPVqZkupBsxyIllEILKHK06" +
|
|
||||||
"OCw49JLTsMR3oTRifGzma79I71X0spw0fM+cIRlkS2tsXN8GPbdkJwHofZKPOXS51pgC1zU8uWX" +
|
|
||||||
"I+ftJO46a1XkNh1dO2anUiQ8P/H4yOTqnMsXF7biyYuiwjXPOcy0OMhEHi54Dq6Mr3u5ZALOAkc" +
|
|
||||||
"YTjh1H/ZgqIHy5ZluahINuDE76qdLYMXrDMQIDAQAB";
|
|
||||||
|
|
||||||
public interface InAppPurchaseListener {
|
public interface InAppPurchaseListener {
|
||||||
void onError(InAppPurchaseTaskType taskType, String error);
|
void onError(InAppPurchaseTaskType taskType, String error);
|
||||||
|
@ -124,6 +87,30 @@ public class InAppPurchaseHelper {
|
||||||
boolean run(InAppPurchaseHelper helper);
|
boolean run(InAppPurchaseHelper helper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class PurchaseInfo {
|
||||||
|
private String sku;
|
||||||
|
private String orderId;
|
||||||
|
private String purchaseToken;
|
||||||
|
|
||||||
|
public PurchaseInfo(String sku, String orderId, String purchaseToken) {
|
||||||
|
this.sku = sku;
|
||||||
|
this.orderId = orderId;
|
||||||
|
this.purchaseToken = purchaseToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSku() {
|
||||||
|
return sku;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrderId() {
|
||||||
|
return orderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPurchaseToken() {
|
||||||
|
return purchaseToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public String getToken() {
|
public String getToken() {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
@ -194,11 +181,7 @@ public class InAppPurchaseHelper {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BillingManager getBillingManager() {
|
protected void exec(final @NonNull InAppPurchaseTaskType taskType, final @NonNull InAppRunnable runnable) {
|
||||||
return billingManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void exec(final @NonNull InAppPurchaseTaskType taskType, final @NonNull InAppRunnable runnable) {
|
|
||||||
if (isDeveloperVersion || !Version.isGooglePlayEnabled(ctx)) {
|
if (isDeveloperVersion || !Version.isGooglePlayEnabled(ctx)) {
|
||||||
notifyDismissProgress(taskType);
|
notifyDismissProgress(taskType);
|
||||||
stop(true);
|
stop(true);
|
||||||
|
@ -222,117 +205,15 @@ public class InAppPurchaseHelper {
|
||||||
try {
|
try {
|
||||||
processingTask = true;
|
processingTask = true;
|
||||||
activeTask = taskType;
|
activeTask = taskType;
|
||||||
billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingUpdatesListener() {
|
execImpl(taskType, runnable);
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBillingClientSetupFinished() {
|
|
||||||
logDebug("Setup finished.");
|
|
||||||
|
|
||||||
BillingManager billingManager = getBillingManager();
|
|
||||||
// Have we been disposed of in the meantime? If so, quit.
|
|
||||||
if (billingManager == null) {
|
|
||||||
stop(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!billingManager.isServiceConnected()) {
|
|
||||||
// Oh noes, there was a problem.
|
|
||||||
//complain("Problem setting up in-app billing: " + result);
|
|
||||||
notifyError(taskType, billingManager.getBillingClientResponseMessage());
|
|
||||||
stop(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
processingTask = !runnable.run(InAppPurchaseHelper.this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConsumeFinished(String token, BillingResult billingResult) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPurchasesUpdated(final List<Purchase> purchases) {
|
|
||||||
|
|
||||||
BillingManager billingManager = getBillingManager();
|
|
||||||
// Have we been disposed of in the meantime? If so, quit.
|
|
||||||
if (billingManager == null) {
|
|
||||||
stop(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTask == InAppPurchaseTaskType.REQUEST_INVENTORY) {
|
|
||||||
List<String> skuInApps = new ArrayList<>();
|
|
||||||
for (InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) {
|
|
||||||
skuInApps.add(purchase.getSku());
|
|
||||||
}
|
|
||||||
for (Purchase p : purchases) {
|
|
||||||
skuInApps.add(p.getSku());
|
|
||||||
}
|
|
||||||
billingManager.querySkuDetailsAsync(SkuType.INAPP, skuInApps, new SkuDetailsResponseListener() {
|
|
||||||
@Override
|
|
||||||
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListInApps) {
|
|
||||||
// Is it a failure?
|
|
||||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
|
||||||
logError("Failed to query inapps sku details: " + billingResult.getResponseCode());
|
|
||||||
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
|
||||||
stop(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> skuSubscriptions = new ArrayList<>();
|
|
||||||
for (InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) {
|
|
||||||
skuSubscriptions.add(subscription.getSku());
|
|
||||||
}
|
|
||||||
for (Purchase p : purchases) {
|
|
||||||
skuSubscriptions.add(p.getSku());
|
|
||||||
}
|
|
||||||
|
|
||||||
BillingManager billingManager = getBillingManager();
|
|
||||||
// Have we been disposed of in the meantime? If so, quit.
|
|
||||||
if (billingManager == null) {
|
|
||||||
stop(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
billingManager.querySkuDetailsAsync(SkuType.SUBS, skuSubscriptions, new SkuDetailsResponseListener() {
|
|
||||||
@Override
|
|
||||||
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListSubscriptions) {
|
|
||||||
// Is it a failure?
|
|
||||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
|
||||||
logError("Failed to query subscriptipons sku details: " + billingResult.getResponseCode());
|
|
||||||
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
|
||||||
stop(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<SkuDetails> skuDetailsList = new ArrayList<>(skuDetailsListInApps);
|
|
||||||
skuDetailsList.addAll(skuDetailsListSubscriptions);
|
|
||||||
InAppPurchaseHelper.this.skuDetailsList = skuDetailsList;
|
|
||||||
|
|
||||||
mSkuDetailsResponseListener.onSkuDetailsResponse(billingResult, skuDetailsList);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (Purchase purchase : purchases) {
|
|
||||||
if (!purchase.isAcknowledged()) {
|
|
||||||
onPurchaseFinished(purchase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPurchaseCanceled() {
|
|
||||||
stop(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logError("exec Error", e);
|
logError("exec Error", e);
|
||||||
stop(true);
|
stop(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppRunnable runnable);
|
||||||
|
|
||||||
public boolean needRequestInventory() {
|
public boolean needRequestInventory() {
|
||||||
return !inventoryRequested && ((isSubscribedToLiveUpdates(ctx) && Algorithms.isEmpty(ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.get()))
|
return !inventoryRequested && ((isSubscribedToLiveUpdates(ctx) && Algorithms.isEmpty(ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.get()))
|
||||||
|| System.currentTimeMillis() - lastValidationCheckTime > PURCHASE_VALIDATION_PERIOD_MSEC);
|
|| System.currentTimeMillis() - lastValidationCheckTime > PURCHASE_VALIDATION_PERIOD_MSEC);
|
||||||
|
@ -343,32 +224,7 @@ public class InAppPurchaseHelper {
|
||||||
new RequestInventoryTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
|
new RequestInventoryTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void purchaseFullVersion(final Activity activity) {
|
public abstract void purchaseFullVersion(final Activity activity) throws UnsupportedOperationException;
|
||||||
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION);
|
|
||||||
exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, new InAppRunnable() {
|
|
||||||
@Override
|
|
||||||
public boolean run(InAppPurchaseHelper helper) {
|
|
||||||
try {
|
|
||||||
SkuDetails skuDetails = getSkuDetails(getFullVersion().getSku());
|
|
||||||
if (skuDetails == null) {
|
|
||||||
throw new IllegalArgumentException("Cannot find sku details");
|
|
||||||
}
|
|
||||||
BillingManager billingManager = getBillingManager();
|
|
||||||
if (billingManager != null) {
|
|
||||||
billingManager.initiatePurchaseFlow(activity, skuDetails);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("BillingManager disposed");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (Exception e) {
|
|
||||||
complain("Cannot launch full version purchase!");
|
|
||||||
logError("purchaseFullVersion Error", e);
|
|
||||||
stop(true);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void purchaseLiveUpdates(Activity activity, String sku, String email, String userName,
|
public void purchaseLiveUpdates(Activity activity, String sku, String email, String userName,
|
||||||
String countryDownloadName, boolean hideUserName) {
|
String countryDownloadName, boolean hideUserName) {
|
||||||
|
@ -377,288 +233,7 @@ public class InAppPurchaseHelper {
|
||||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
|
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void purchaseDepthContours(final Activity activity) {
|
public abstract void purchaseDepthContours(final Activity activity) throws UnsupportedOperationException;
|
||||||
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS);
|
|
||||||
exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, new InAppRunnable() {
|
|
||||||
@Override
|
|
||||||
public boolean run(InAppPurchaseHelper helper) {
|
|
||||||
try {
|
|
||||||
SkuDetails skuDetails = getSkuDetails(getDepthContours().getSku());
|
|
||||||
if (skuDetails == null) {
|
|
||||||
throw new IllegalArgumentException("Cannot find sku details");
|
|
||||||
}
|
|
||||||
BillingManager billingManager = getBillingManager();
|
|
||||||
if (billingManager != null) {
|
|
||||||
billingManager.initiatePurchaseFlow(activity, skuDetails);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("BillingManager disposed");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (Exception e) {
|
|
||||||
complain("Cannot launch depth contours purchase!");
|
|
||||||
logError("purchaseDepthContours Error", e);
|
|
||||||
stop(true);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private SkuDetails getSkuDetails(@NonNull String sku) {
|
|
||||||
List<SkuDetails> skuDetailsList = this.skuDetailsList;
|
|
||||||
if (skuDetailsList != null) {
|
|
||||||
for (SkuDetails details : skuDetailsList) {
|
|
||||||
if (details.getSku().equals(sku)) {
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasDetails(@NonNull String sku) {
|
|
||||||
return getSkuDetails(sku) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Purchase getPurchase(@NonNull String sku) {
|
|
||||||
BillingManager billingManager = getBillingManager();
|
|
||||||
if (billingManager != null) {
|
|
||||||
List<Purchase> purchases = billingManager.getPurchases();
|
|
||||||
if (purchases != null) {
|
|
||||||
for (Purchase p : purchases) {
|
|
||||||
if (p.getSku().equals(sku)) {
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listener that's called when we finish querying the items and subscriptions we own
|
|
||||||
private SkuDetailsResponseListener mSkuDetailsResponseListener = new SkuDetailsResponseListener() {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private List<String> getAllOwnedSubscriptionSkus() {
|
|
||||||
List<String> result = new ArrayList<>();
|
|
||||||
BillingManager billingManager = getBillingManager();
|
|
||||||
if (billingManager != null) {
|
|
||||||
for (Purchase p : billingManager.getPurchases()) {
|
|
||||||
if (getInAppPurchases().getInAppSubscriptionBySku(p.getSku()) != null) {
|
|
||||||
result.add(p.getSku());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
|
||||||
|
|
||||||
logDebug("Query sku details finished.");
|
|
||||||
|
|
||||||
// Have we been disposed of in the meantime? If so, quit.
|
|
||||||
if (getBillingManager() == null) {
|
|
||||||
stop(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is it a failure?
|
|
||||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
|
||||||
logError("Failed to query inventory: " + billingResult.getResponseCode());
|
|
||||||
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
|
||||||
stop(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logDebug("Query sku details was successful.");
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Check for items we own. Notice that for each purchase, we check
|
|
||||||
* the developer payload to see if it's correct! See
|
|
||||||
* verifyDeveloperPayload().
|
|
||||||
*/
|
|
||||||
|
|
||||||
List<String> allOwnedSubscriptionSkus = getAllOwnedSubscriptionSkus();
|
|
||||||
for (InAppSubscription s : getLiveUpdates().getAllSubscriptions()) {
|
|
||||||
if (hasDetails(s.getSku())) {
|
|
||||||
Purchase purchase = getPurchase(s.getSku());
|
|
||||||
SkuDetails liveUpdatesDetails = getSkuDetails(s.getSku());
|
|
||||||
if (liveUpdatesDetails != null) {
|
|
||||||
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
|
|
||||||
}
|
|
||||||
allOwnedSubscriptionSkus.remove(s.getSku());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (String sku : allOwnedSubscriptionSkus) {
|
|
||||||
Purchase purchase = getPurchase(sku);
|
|
||||||
SkuDetails liveUpdatesDetails = getSkuDetails(sku);
|
|
||||||
if (liveUpdatesDetails != null) {
|
|
||||||
InAppSubscription s = getLiveUpdates().upgradeSubscription(sku);
|
|
||||||
if (s == null) {
|
|
||||||
s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails);
|
|
||||||
}
|
|
||||||
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InAppPurchase fullVersion = getFullVersion();
|
|
||||||
if (hasDetails(fullVersion.getSku())) {
|
|
||||||
Purchase purchase = getPurchase(fullVersion.getSku());
|
|
||||||
SkuDetails fullPriceDetails = getSkuDetails(fullVersion.getSku());
|
|
||||||
if (fullPriceDetails != null) {
|
|
||||||
fetchInAppPurchase(fullVersion, fullPriceDetails, purchase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InAppPurchase depthContours = getDepthContours();
|
|
||||||
if (hasDetails(depthContours.getSku())) {
|
|
||||||
Purchase purchase = getPurchase(depthContours.getSku());
|
|
||||||
SkuDetails depthContoursDetails = getSkuDetails(depthContours.getSku());
|
|
||||||
if (depthContoursDetails != null) {
|
|
||||||
fetchInAppPurchase(depthContours, depthContoursDetails, purchase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InAppPurchase contourLines = getContourLines();
|
|
||||||
if (hasDetails(contourLines.getSku())) {
|
|
||||||
Purchase purchase = getPurchase(contourLines.getSku());
|
|
||||||
SkuDetails contourLinesDetails = getSkuDetails(contourLines.getSku());
|
|
||||||
if (contourLinesDetails != null) {
|
|
||||||
fetchInAppPurchase(contourLines, contourLinesDetails, purchase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Purchase fullVersionPurchase = getPurchase(fullVersion.getSku());
|
|
||||||
boolean fullVersionPurchased = fullVersionPurchase != null;
|
|
||||||
if (fullVersionPurchased) {
|
|
||||||
ctx.getSettings().FULL_VERSION_PURCHASED.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Purchase depthContoursPurchase = getPurchase(depthContours.getSku());
|
|
||||||
boolean depthContoursPurchased = depthContoursPurchase != null;
|
|
||||||
if (depthContoursPurchased) {
|
|
||||||
ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do we have the live updates?
|
|
||||||
boolean subscribedToLiveUpdates = false;
|
|
||||||
List<Purchase> liveUpdatesPurchases = new ArrayList<>();
|
|
||||||
for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) {
|
|
||||||
Purchase purchase = getPurchase(p.getSku());
|
|
||||||
if (purchase != null) {
|
|
||||||
liveUpdatesPurchases.add(purchase);
|
|
||||||
if (!subscribedToLiveUpdates) {
|
|
||||||
subscribedToLiveUpdates = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OsmandPreference<Long> subscriptionCancelledTime = ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME;
|
|
||||||
if (!subscribedToLiveUpdates && ctx.getSettings().LIVE_UPDATES_PURCHASED.get()) {
|
|
||||||
if (subscriptionCancelledTime.get() == 0) {
|
|
||||||
subscriptionCancelledTime.set(System.currentTimeMillis());
|
|
||||||
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false);
|
|
||||||
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false);
|
|
||||||
} else if (System.currentTimeMillis() - subscriptionCancelledTime.get() > SUBSCRIPTION_HOLDING_TIME_MSEC) {
|
|
||||||
ctx.getSettings().LIVE_UPDATES_PURCHASED.set(false);
|
|
||||||
if (!isDepthContoursPurchased(ctx)) {
|
|
||||||
ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (subscribedToLiveUpdates) {
|
|
||||||
subscriptionCancelledTime.set(0L);
|
|
||||||
ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastValidationCheckTime = System.currentTimeMillis();
|
|
||||||
logDebug("User " + (subscribedToLiveUpdates ? "HAS" : "DOES NOT HAVE")
|
|
||||||
+ " live updates purchased.");
|
|
||||||
|
|
||||||
OsmandSettings settings = ctx.getSettings();
|
|
||||||
settings.INAPPS_READ.set(true);
|
|
||||||
|
|
||||||
List<Purchase> tokensToSend = new ArrayList<>();
|
|
||||||
if (liveUpdatesPurchases.size() > 0) {
|
|
||||||
List<String> tokensSent = Arrays.asList(settings.BILLING_PURCHASE_TOKENS_SENT.get().split(";"));
|
|
||||||
for (Purchase purchase : liveUpdatesPurchases) {
|
|
||||||
if ((Algorithms.isEmpty(settings.BILLING_USER_ID.get()) || Algorithms.isEmpty(settings.BILLING_USER_TOKEN.get()))
|
|
||||||
&& !Algorithms.isEmpty(purchase.getDeveloperPayload())) {
|
|
||||||
String payload = purchase.getDeveloperPayload();
|
|
||||||
if (!Algorithms.isEmpty(payload)) {
|
|
||||||
String[] arr = payload.split(" ");
|
|
||||||
if (arr.length > 0) {
|
|
||||||
settings.BILLING_USER_ID.set(arr[0]);
|
|
||||||
}
|
|
||||||
if (arr.length > 1) {
|
|
||||||
token = arr[1];
|
|
||||||
settings.BILLING_USER_TOKEN.set(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!tokensSent.contains(purchase.getSku())) {
|
|
||||||
tokensToSend.add(purchase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final OnRequestResultListener listener = new OnRequestResultListener() {
|
|
||||||
@Override
|
|
||||||
public void onResult(String result) {
|
|
||||||
notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY);
|
|
||||||
notifyGetItems();
|
|
||||||
stop(true);
|
|
||||||
logDebug("Initial inapp query finished");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tokensToSend.size() > 0) {
|
|
||||||
sendTokens(tokensToSend, listener);
|
|
||||||
} else {
|
|
||||||
listener.onResult("OK");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void fetchInAppPurchase(@NonNull InAppPurchase inAppPurchase, @NonNull SkuDetails skuDetails, @Nullable Purchase purchase) {
|
|
||||||
if (purchase != null) {
|
|
||||||
inAppPurchase.setPurchaseState(PurchaseState.PURCHASED);
|
|
||||||
inAppPurchase.setPurchaseTime(purchase.getPurchaseTime());
|
|
||||||
} else {
|
|
||||||
inAppPurchase.setPurchaseState(PurchaseState.NOT_PURCHASED);
|
|
||||||
}
|
|
||||||
inAppPurchase.setPrice(skuDetails.getPrice());
|
|
||||||
inAppPurchase.setPriceCurrencyCode(skuDetails.getPriceCurrencyCode());
|
|
||||||
if (skuDetails.getPriceAmountMicros() > 0) {
|
|
||||||
inAppPurchase.setPriceValue(skuDetails.getPriceAmountMicros() / 1000000d);
|
|
||||||
}
|
|
||||||
String subscriptionPeriod = skuDetails.getSubscriptionPeriod();
|
|
||||||
if (!Algorithms.isEmpty(subscriptionPeriod)) {
|
|
||||||
if (inAppPurchase instanceof InAppSubscription) {
|
|
||||||
try {
|
|
||||||
((InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod);
|
|
||||||
} catch (ParseException e) {
|
|
||||||
LOG.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inAppPurchase instanceof InAppSubscription) {
|
|
||||||
String introductoryPrice = skuDetails.getIntroductoryPrice();
|
|
||||||
String introductoryPricePeriod = skuDetails.getIntroductoryPricePeriod();
|
|
||||||
String introductoryPriceCycles = skuDetails.getIntroductoryPriceCycles();
|
|
||||||
long introductoryPriceAmountMicros = skuDetails.getIntroductoryPriceAmountMicros();
|
|
||||||
if (!Algorithms.isEmpty(introductoryPrice)) {
|
|
||||||
InAppSubscription s = (InAppSubscription) inAppPurchase;
|
|
||||||
try {
|
|
||||||
s.setIntroductoryInfo(new InAppSubscriptionIntroductoryInfo(s, introductoryPrice,
|
|
||||||
introductoryPriceAmountMicros, introductoryPricePeriod, introductoryPriceCycles));
|
|
||||||
} catch (ParseException e) {
|
|
||||||
LOG.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private class LiveUpdatesPurchaseTask extends AsyncTask<Void, Void, String> {
|
private class LiveUpdatesPurchaseTask extends AsyncTask<Void, Void, String> {
|
||||||
|
@ -746,31 +321,7 @@ public class InAppPurchaseHelper {
|
||||||
if (!Algorithms.isEmpty(userId) && !Algorithms.isEmpty(token)) {
|
if (!Algorithms.isEmpty(userId) && !Algorithms.isEmpty(token)) {
|
||||||
logDebug("Launching purchase flow for live updates subscription for userId=" + userId);
|
logDebug("Launching purchase flow for live updates subscription for userId=" + userId);
|
||||||
final String payload = userId + " " + token;
|
final String payload = userId + " " + token;
|
||||||
exec(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, new InAppRunnable() {
|
exec(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, getPurchaseLiveUpdatesCommand(activity, sku, payload));
|
||||||
@Override
|
|
||||||
public boolean run(InAppPurchaseHelper helper) {
|
|
||||||
try {
|
|
||||||
Activity a = activity.get();
|
|
||||||
SkuDetails skuDetails = getSkuDetails(sku);
|
|
||||||
if (a != null && skuDetails != null) {
|
|
||||||
BillingManager billingManager = getBillingManager();
|
|
||||||
if (billingManager != null) {
|
|
||||||
billingManager.setPayload(payload);
|
|
||||||
billingManager.initiatePurchaseFlow(a, skuDetails);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("BillingManager disposed");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
stop(true);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logError("launchPurchaseFlow Error", e);
|
|
||||||
stop(true);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
notifyError(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, "Empty userId");
|
notifyError(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, "Empty userId");
|
||||||
stop(true);
|
stop(true);
|
||||||
|
@ -778,6 +329,9 @@ public class InAppPurchaseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract InAppRunnable getPurchaseLiveUpdatesCommand(final WeakReference<Activity> activity,
|
||||||
|
final String sku, final String payload) throws UnsupportedOperationException;
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private class RequestInventoryTask extends AsyncTask<Void, Void, String> {
|
private class RequestInventoryTask extends AsyncTask<Void, Void, String> {
|
||||||
|
|
||||||
|
@ -808,6 +362,7 @@ public class InAppPurchaseHelper {
|
||||||
try {
|
try {
|
||||||
JSONObject obj = new JSONObject(response);
|
JSONObject obj = new JSONObject(response);
|
||||||
JSONArray names = obj.names();
|
JSONArray names = obj.names();
|
||||||
|
if (names != null) {
|
||||||
for (int i = 0; i < names.length(); i++) {
|
for (int i = 0; i < names.length(); i++) {
|
||||||
String skuType = names.getString(i);
|
String skuType = names.getString(i);
|
||||||
JSONObject subObj = obj.getJSONObject(skuType);
|
JSONObject subObj = obj.getJSONObject(skuType);
|
||||||
|
@ -816,30 +371,32 @@ public class InAppPurchaseHelper {
|
||||||
getLiveUpdates().upgradeSubscription(sku);
|
getLiveUpdates().upgradeSubscription(sku);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
logError("Json parsing error", e);
|
logError("Json parsing error", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exec(InAppPurchaseTaskType.REQUEST_INVENTORY, new InAppRunnable() {
|
exec(InAppPurchaseTaskType.REQUEST_INVENTORY, getRequestInventoryCommand());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract InAppRunnable getRequestInventoryCommand() throws UnsupportedOperationException;
|
||||||
|
|
||||||
|
protected void onSkuDetailsResponseDone(List<PurchaseInfo> purchaseInfoList) {
|
||||||
|
final AndroidNetworkUtils.OnRequestResultListener listener = new AndroidNetworkUtils.OnRequestResultListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean run(InAppPurchaseHelper helper) {
|
public void onResult(String result) {
|
||||||
logDebug("Setup successful. Querying inventory.");
|
|
||||||
try {
|
|
||||||
BillingManager billingManager = getBillingManager();
|
|
||||||
if (billingManager != null) {
|
|
||||||
billingManager.queryPurchases();
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("BillingManager disposed");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (Exception e) {
|
|
||||||
logError("queryInventoryAsync Error", e);
|
|
||||||
notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY);
|
notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY);
|
||||||
|
notifyGetItems();
|
||||||
stop(true);
|
stop(true);
|
||||||
|
logDebug("Initial inapp query finished");
|
||||||
}
|
}
|
||||||
return true;
|
};
|
||||||
}
|
|
||||||
});
|
if (purchaseInfoList.size() > 0) {
|
||||||
|
sendTokens(purchaseInfoList, listener);
|
||||||
|
} else {
|
||||||
|
listener.onResult("OK");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -852,25 +409,16 @@ public class InAppPurchaseHelper {
|
||||||
parameters.put("aid", ctx.getUserAndroidId());
|
parameters.put("aid", ctx.getUserAndroidId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call when a purchase is finished
|
protected void onPurchaseDone(PurchaseInfo info) {
|
||||||
private void onPurchaseFinished(Purchase purchase) {
|
|
||||||
logDebug("Purchase finished: " + purchase);
|
|
||||||
|
|
||||||
// if we were disposed of in the meantime, quit.
|
|
||||||
if (getBillingManager() == null) {
|
|
||||||
stop(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logDebug("Purchase successful.");
|
logDebug("Purchase successful.");
|
||||||
|
|
||||||
InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(purchase.getSku());
|
InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(info.getSku());
|
||||||
if (liveUpdatesPurchase != null) {
|
if (liveUpdatesPurchase != null) {
|
||||||
// bought live updates
|
// bought live updates
|
||||||
logDebug("Live updates subscription purchased.");
|
logDebug("Live updates subscription purchased.");
|
||||||
final String sku = liveUpdatesPurchase.getSku();
|
final String sku = liveUpdatesPurchase.getSku();
|
||||||
liveUpdatesPurchase.setPurchaseState(PurchaseState.PURCHASED);
|
liveUpdatesPurchase.setPurchaseState(PurchaseState.PURCHASED);
|
||||||
sendTokens(Collections.singletonList(purchase), new OnRequestResultListener() {
|
sendTokens(Collections.singletonList(info), new OnRequestResultListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onResult(String result) {
|
public void onResult(String result) {
|
||||||
boolean active = ctx.getSettings().LIVE_UPDATES_PURCHASED.get();
|
boolean active = ctx.getSettings().LIVE_UPDATES_PURCHASED.get();
|
||||||
|
@ -887,7 +435,7 @@ public class InAppPurchaseHelper {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} else if (purchase.getSku().equals(getFullVersion().getSku())) {
|
} else if (info.getSku().equals(getFullVersion().getSku())) {
|
||||||
// bought full version
|
// bought full version
|
||||||
getFullVersion().setPurchaseState(PurchaseState.PURCHASED);
|
getFullVersion().setPurchaseState(PurchaseState.PURCHASED);
|
||||||
logDebug("Full version purchased.");
|
logDebug("Full version purchased.");
|
||||||
|
@ -898,7 +446,7 @@ public class InAppPurchaseHelper {
|
||||||
notifyItemPurchased(getFullVersion().getSku(), false);
|
notifyItemPurchased(getFullVersion().getSku(), false);
|
||||||
stop(true);
|
stop(true);
|
||||||
|
|
||||||
} else if (purchase.getSku().equals(getDepthContours().getSku())) {
|
} else if (info.getSku().equals(getDepthContours().getSku())) {
|
||||||
// bought sea depth contours
|
// bought sea depth contours
|
||||||
getDepthContours().setPurchaseState(PurchaseState.PURCHASED);
|
getDepthContours().setPurchaseState(PurchaseState.PURCHASED);
|
||||||
logDebug("Sea depth contours purchased.");
|
logDebug("Sea depth contours purchased.");
|
||||||
|
@ -921,17 +469,19 @@ public class InAppPurchaseHelper {
|
||||||
stop(false);
|
stop(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stop(boolean taskDone) {
|
protected abstract boolean isBillingManagerExists();
|
||||||
|
|
||||||
|
protected abstract void destroyBillingManager();
|
||||||
|
|
||||||
|
protected void stop(boolean taskDone) {
|
||||||
logDebug("Destroying helper.");
|
logDebug("Destroying helper.");
|
||||||
BillingManager billingManager = getBillingManager();
|
if (isBillingManagerExists()) {
|
||||||
if (billingManager != null) {
|
|
||||||
if (taskDone) {
|
if (taskDone) {
|
||||||
processingTask = false;
|
processingTask = false;
|
||||||
}
|
}
|
||||||
if (!processingTask) {
|
if (!processingTask) {
|
||||||
activeTask = null;
|
activeTask = null;
|
||||||
billingManager.destroy();
|
destroyBillingManager();
|
||||||
this.billingManager = null;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
processingTask = false;
|
processingTask = false;
|
||||||
|
@ -943,7 +493,7 @@ public class InAppPurchaseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendTokens(@NonNull final List<Purchase> purchases, final OnRequestResultListener listener) {
|
protected void sendTokens(@NonNull final List<PurchaseInfo> purchaseInfoList, final OnRequestResultListener listener) {
|
||||||
final String userId = ctx.getSettings().BILLING_USER_ID.get();
|
final String userId = ctx.getSettings().BILLING_USER_ID.get();
|
||||||
final String token = ctx.getSettings().BILLING_USER_TOKEN.get();
|
final String token = ctx.getSettings().BILLING_USER_TOKEN.get();
|
||||||
final String email = ctx.getSettings().BILLING_USER_EMAIL.get();
|
final String email = ctx.getSettings().BILLING_USER_EMAIL.get();
|
||||||
|
@ -951,12 +501,12 @@ public class InAppPurchaseHelper {
|
||||||
String url = "https://osmand.net/subscription/purchased";
|
String url = "https://osmand.net/subscription/purchased";
|
||||||
String userOperation = "Sending purchase info...";
|
String userOperation = "Sending purchase info...";
|
||||||
final List<AndroidNetworkUtils.Request> requests = new ArrayList<>();
|
final List<AndroidNetworkUtils.Request> requests = new ArrayList<>();
|
||||||
for (Purchase purchase : purchases) {
|
for (PurchaseInfo info : purchaseInfoList) {
|
||||||
Map<String, String> parameters = new HashMap<>();
|
Map<String, String> parameters = new HashMap<>();
|
||||||
parameters.put("userid", userId);
|
parameters.put("userid", userId);
|
||||||
parameters.put("sku", purchase.getSku());
|
parameters.put("sku", info.getSku());
|
||||||
parameters.put("orderId", purchase.getOrderId());
|
parameters.put("orderId", info.getOrderId());
|
||||||
parameters.put("purchaseToken", purchase.getPurchaseToken());
|
parameters.put("purchaseToken", info.getPurchaseToken());
|
||||||
parameters.put("email", email);
|
parameters.put("email", email);
|
||||||
parameters.put("token", token);
|
parameters.put("token", token);
|
||||||
addUserInfo(parameters);
|
addUserInfo(parameters);
|
||||||
|
@ -967,9 +517,9 @@ public class InAppPurchaseHelper {
|
||||||
public void onResult(@NonNull List<RequestResponse> results) {
|
public void onResult(@NonNull List<RequestResponse> results) {
|
||||||
for (RequestResponse rr : results) {
|
for (RequestResponse rr : results) {
|
||||||
String sku = rr.getRequest().getParameters().get("sku");
|
String sku = rr.getRequest().getParameters().get("sku");
|
||||||
Purchase purchase = getPurchase(sku);
|
PurchaseInfo info = getPurchaseInfo(sku);
|
||||||
if (purchase != null) {
|
if (info != null) {
|
||||||
updateSentTokens(purchase);
|
updateSentTokens(info);
|
||||||
String result = rr.getResponse();
|
String result = rr.getResponse();
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -979,13 +529,13 @@ public class InAppPurchaseHelper {
|
||||||
} else {
|
} else {
|
||||||
complain("SendToken Error: "
|
complain("SendToken Error: "
|
||||||
+ obj.getString("error")
|
+ obj.getString("error")
|
||||||
+ " (userId=" + userId + " token=" + token + " response=" + result + " google=" + purchase.toString() + ")");
|
+ " (userId=" + userId + " token=" + token + " response=" + result + " google=" + info.toString() + ")");
|
||||||
}
|
}
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
logError("SendToken", e);
|
logError("SendToken", e);
|
||||||
complain("SendToken Error: "
|
complain("SendToken Error: "
|
||||||
+ (e.getMessage() != null ? e.getMessage() : "JSONException")
|
+ (e.getMessage() != null ? e.getMessage() : "JSONException")
|
||||||
+ " (userId=" + userId + " token=" + token + " response=" + result + " google=" + purchase.toString() + ")");
|
+ " (userId=" + userId + " token=" + token + " response=" + result + " google=" + info.toString() + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -995,10 +545,10 @@ public class InAppPurchaseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSentTokens(@NonNull Purchase purchase) {
|
private void updateSentTokens(@NonNull PurchaseInfo info) {
|
||||||
String tokensSentStr = ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.get();
|
String tokensSentStr = ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.get();
|
||||||
Set<String> tokensSent = new HashSet<>(Arrays.asList(tokensSentStr.split(";")));
|
Set<String> tokensSent = new HashSet<>(Arrays.asList(tokensSentStr.split(";")));
|
||||||
tokensSent.add(purchase.getSku());
|
tokensSent.add(info.getSku());
|
||||||
ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.set(TextUtils.join(";", tokensSent));
|
ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.set(TextUtils.join(";", tokensSent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1032,10 +582,10 @@ public class InAppPurchaseHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private Purchase getPurchase(String sku) {
|
private PurchaseInfo getPurchaseInfo(String sku) {
|
||||||
for (Purchase purchase : purchases) {
|
for (PurchaseInfo info : purchaseInfoList) {
|
||||||
if (purchase.getSku().equals(sku)) {
|
if (info.getSku().equals(sku)) {
|
||||||
return purchase;
|
return info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -1049,31 +599,31 @@ public class InAppPurchaseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyError(InAppPurchaseTaskType taskType, String message) {
|
protected void notifyError(InAppPurchaseTaskType taskType, String message) {
|
||||||
if (uiActivity != null) {
|
if (uiActivity != null) {
|
||||||
uiActivity.onError(taskType, message);
|
uiActivity.onError(taskType, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyGetItems() {
|
protected void notifyGetItems() {
|
||||||
if (uiActivity != null) {
|
if (uiActivity != null) {
|
||||||
uiActivity.onGetItems();
|
uiActivity.onGetItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyItemPurchased(String sku, boolean active) {
|
protected void notifyItemPurchased(String sku, boolean active) {
|
||||||
if (uiActivity != null) {
|
if (uiActivity != null) {
|
||||||
uiActivity.onItemPurchased(sku, active);
|
uiActivity.onItemPurchased(sku, active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyShowProgress(InAppPurchaseTaskType taskType) {
|
protected void notifyShowProgress(InAppPurchaseTaskType taskType) {
|
||||||
if (uiActivity != null) {
|
if (uiActivity != null) {
|
||||||
uiActivity.showProgress(taskType);
|
uiActivity.showProgress(taskType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyDismissProgress(InAppPurchaseTaskType taskType) {
|
protected void notifyDismissProgress(InAppPurchaseTaskType taskType) {
|
||||||
if (uiActivity != null) {
|
if (uiActivity != null) {
|
||||||
uiActivity.dismissProgress(taskType);
|
uiActivity.dismissProgress(taskType);
|
||||||
}
|
}
|
||||||
|
@ -1090,26 +640,26 @@ public class InAppPurchaseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void complain(String message) {
|
protected void complain(String message) {
|
||||||
logError("**** InAppPurchaseHelper Error: " + message);
|
logError("**** InAppPurchaseHelper Error: " + message);
|
||||||
showToast(message);
|
showToast(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showToast(final String message) {
|
protected void showToast(final String message) {
|
||||||
ctx.showToastMessage(message);
|
ctx.showToastMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logDebug(String msg) {
|
protected void logDebug(String msg) {
|
||||||
if (mDebugLog) {
|
if (mDebugLog) {
|
||||||
Log.d(TAG, msg);
|
Log.d(TAG, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logError(String msg) {
|
protected void logError(String msg) {
|
||||||
Log.e(TAG, msg);
|
Log.e(TAG, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logError(String msg, Throwable e) {
|
protected void logError(String msg, Throwable e) {
|
||||||
Log.e(TAG, "Error: " + msg, e);
|
Log.e(TAG, "Error: " + msg, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,12 @@ public class InAppPurchases {
|
||||||
new InAppPurchaseLiveUpdatesAnnualFree()
|
new InAppPurchaseLiveUpdatesAnnualFree()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static final InAppSubscription[] LIVE_UPDATES_HW_FREE = new InAppSubscription[]{
|
||||||
|
new InAppPurchaseLiveUpdatesMonthlyHWFree(),
|
||||||
|
new InAppPurchaseLiveUpdates3MonthsHWFree(),
|
||||||
|
new InAppPurchaseLiveUpdatesAnnualHWFree()
|
||||||
|
};
|
||||||
|
|
||||||
private InAppPurchase fullVersion;
|
private InAppPurchase fullVersion;
|
||||||
private InAppPurchase depthContours;
|
private InAppPurchase depthContours;
|
||||||
private InAppPurchase contourLines;
|
private InAppPurchase contourLines;
|
||||||
|
@ -65,7 +71,9 @@ public class InAppPurchases {
|
||||||
|
|
||||||
InAppPurchases(OsmandApplication ctx) {
|
InAppPurchases(OsmandApplication ctx) {
|
||||||
fullVersion = FULL_VERSION;
|
fullVersion = FULL_VERSION;
|
||||||
if (Version.isFreeVersion(ctx)) {
|
if (Version.isHuawei(ctx)) {
|
||||||
|
liveUpdates = new LiveUpdatesInAppPurchasesHWFree();
|
||||||
|
} else if (Version.isFreeVersion(ctx)) {
|
||||||
liveUpdates = new LiveUpdatesInAppPurchasesFree();
|
liveUpdates = new LiveUpdatesInAppPurchasesFree();
|
||||||
} else {
|
} else {
|
||||||
liveUpdates = new LiveUpdatesInAppPurchasesFull();
|
liveUpdates = new LiveUpdatesInAppPurchasesFull();
|
||||||
|
@ -267,6 +275,13 @@ public class InAppPurchases {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class LiveUpdatesInAppPurchasesHWFree extends InAppSubscriptionList {
|
||||||
|
|
||||||
|
public LiveUpdatesInAppPurchasesHWFree() {
|
||||||
|
super(LIVE_UPDATES_HW_FREE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class LiveUpdatesInAppPurchasesFull extends InAppSubscriptionList {
|
public static class LiveUpdatesInAppPurchasesFull extends InAppSubscriptionList {
|
||||||
|
|
||||||
public LiveUpdatesInAppPurchasesFull() {
|
public LiveUpdatesInAppPurchasesFull() {
|
||||||
|
@ -943,6 +958,25 @@ public class InAppPurchases {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class InAppPurchaseLiveUpdatesMonthlyHWFree extends InAppPurchaseLiveUpdatesMonthly {
|
||||||
|
|
||||||
|
private static final String SKU_LIVE_UPDATES_MONTHLY_HW_FREE = "net.osmand.test.monthly";
|
||||||
|
|
||||||
|
InAppPurchaseLiveUpdatesMonthlyHWFree() {
|
||||||
|
super(SKU_LIVE_UPDATES_MONTHLY_HW_FREE, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InAppPurchaseLiveUpdatesMonthlyHWFree(@NonNull String sku) {
|
||||||
|
super(sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected InAppSubscription newInstance(@NonNull String sku) {
|
||||||
|
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyHWFree(sku) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static abstract class InAppPurchaseLiveUpdates3Months extends InAppSubscription {
|
public static abstract class InAppPurchaseLiveUpdates3Months extends InAppSubscription {
|
||||||
|
|
||||||
InAppPurchaseLiveUpdates3Months(String skuNoVersion, int version) {
|
InAppPurchaseLiveUpdates3Months(String skuNoVersion, int version) {
|
||||||
|
@ -1024,6 +1058,25 @@ public class InAppPurchases {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class InAppPurchaseLiveUpdates3MonthsHWFree extends InAppPurchaseLiveUpdates3Months {
|
||||||
|
|
||||||
|
private static final String SKU_LIVE_UPDATES_3_MONTHS_HW_FREE = "net.osmand.test.3months";
|
||||||
|
|
||||||
|
InAppPurchaseLiveUpdates3MonthsHWFree() {
|
||||||
|
super(SKU_LIVE_UPDATES_3_MONTHS_HW_FREE, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InAppPurchaseLiveUpdates3MonthsHWFree(@NonNull String sku) {
|
||||||
|
super(sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected InAppSubscription newInstance(@NonNull String sku) {
|
||||||
|
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsHWFree(sku) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static abstract class InAppPurchaseLiveUpdatesAnnual extends InAppSubscription {
|
public static abstract class InAppPurchaseLiveUpdatesAnnual extends InAppSubscription {
|
||||||
|
|
||||||
InAppPurchaseLiveUpdatesAnnual(String skuNoVersion, int version) {
|
InAppPurchaseLiveUpdatesAnnual(String skuNoVersion, int version) {
|
||||||
|
@ -1105,6 +1158,25 @@ public class InAppPurchases {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class InAppPurchaseLiveUpdatesAnnualHWFree extends InAppPurchaseLiveUpdatesAnnual {
|
||||||
|
|
||||||
|
private static final String SKU_LIVE_UPDATES_ANNUAL_HW_FREE = "net.osmand.test.annual";
|
||||||
|
|
||||||
|
InAppPurchaseLiveUpdatesAnnualHWFree() {
|
||||||
|
super(SKU_LIVE_UPDATES_ANNUAL_HW_FREE, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InAppPurchaseLiveUpdatesAnnualHWFree(@NonNull String sku) {
|
||||||
|
super(sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected InAppSubscription newInstance(@NonNull String sku) {
|
||||||
|
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualHWFree(sku) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class InAppPurchaseLiveUpdatesOldMonthly extends InAppPurchaseLiveUpdatesMonthly {
|
public static class InAppPurchaseLiveUpdatesOldMonthly extends InAppPurchaseLiveUpdatesMonthly {
|
||||||
|
|
||||||
InAppPurchaseLiveUpdatesOldMonthly(String sku) {
|
InAppPurchaseLiveUpdatesOldMonthly(String sku) {
|
||||||
|
|
BIN
OsmAndHms.jks
Normal file
BIN
OsmAndHms.jks
Normal file
Binary file not shown.
1
agconnect-services.json
Normal file
1
agconnect-services.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{ "client":{ "cp_id":"890031000000000038", "product_id":"9105385871708313983", "client_id":"250816257723466880", "client_secret":"FC1C714C19B635505454739C0E3EB87D2B8F09669F31748BA81DB77B5E686F8F", "app_id":"101486545", "package_name":"net.osmand.huawei", "api_key":"CV5WXNxMPZSL29Fs885sxIwCyl5xCy+KTRUGP0mzC1K2poUrcCR/b+1IwGZtQJwiH18AoSOC1N2sxQ/dVtsqN38yrMpa" }, "configuration_version":"1.0" }
|
Loading…
Reference in a new issue