Merge pull request #9942 from osmandapp/huawei

Huawei
This commit is contained in:
max-klaus 2020-10-04 16:09:42 +03:00 committed by GitHub
commit 5fdeeffd2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 2971 additions and 1093 deletions

4
.gitignore vendored
View file

@ -19,6 +19,10 @@ OsmAndCore_*.aar
.project
out/
# Huawei
agconnect-services.json
OsmAndHms.jks
# Android Studio
/.idea
*.iml

3
OsmAnd/.gitignore vendored
View file

@ -13,10 +13,13 @@ libs/it.unibo.alice.tuprolog-tuprolog-3.2.1.jar
libs/commons-codec-commons-codec-1.11.jar
libs/OsmAndCore_android-0.1-SNAPSHOT.jar
# Huawei
libs/huawei-*.jar
huaweidrmlib/
HwDRM_SDK_*
drm_strings.xml
agconnect-services.json
OsmAndHms.jks
# copy_widget_icons.sh
res/drawable-large/map_*

View file

@ -2,24 +2,24 @@
<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>
<application
android:icon="@mipmap/icon_free"
android:label="@string/app_name_free"
tools:replace="android:icon, android:label">
<activity
android:name="net.osmand.plus.activities.MapActivity"
android:theme="@style/FirstSplashScreenFree"
tools:replace="android:theme"/>
<service
android:name="net.osmand.plus.NavigationService"
tools:replace="android:process"
android:process="net.osmand.huawei"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="net.osmand.huawei.fileprovider"
tools:replace="android:authorities" />
<service
android:name="net.osmand.plus.NavigationService"
android:process="net.osmand.huawei"
tools:replace="android:process" />
tools:replace="android:authorities"
android:authorities="net.osmand.huawei.fileprovider"/>
</application>
</manifest>

View file

@ -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>

View file

@ -40,6 +40,13 @@ android {
keyAlias "osmand"
keyPassword System.getenv("OSMAND_APK_PASSWORD")
}
publishingHuawei {
storeFile file("/var/lib/jenkins/osmand_hw_key")
storePassword System.getenv("OSMAND_HW_APK_PASSWORD")
keyAlias "osmand"
keyPassword System.getenv("OSMAND_HW_APK_PASSWORD")
}
}
defaultConfig {
@ -107,19 +114,23 @@ android {
debug {
manifest.srcFile "AndroidManifest-debug.xml"
}
full {
java.srcDirs = ["src-google"]
}
free {
java.srcDirs = ["src-google"]
manifest.srcFile "AndroidManifest-free.xml"
}
freedev {
java.srcDirs = ["src-google"]
manifest.srcFile "AndroidManifest-freedev.xml"
}
freecustom {
java.srcDirs = ["src-google"]
manifest.srcFile "AndroidManifest-freecustom.xml"
}
huawei {
manifest.srcFile "AndroidManifest-huawei.xml"
}
freehuawei {
java.srcDirs = ["src-huawei"]
manifest.srcFile "AndroidManifest-freehuawei.xml"
}
@ -190,10 +201,6 @@ android {
applicationId "net.osmand.plus"
resConfig "en"
//resConfigs "xxhdpi", "nodpi"
}
huawei {
dimension "version"
applicationId "net.osmand.plus.huawei"
}
freehuawei {
dimension "version"
@ -219,7 +226,10 @@ android {
signingConfig signingConfigs.development
}
release {
signingConfig signingConfigs.publishing
productFlavors.all { flavor ->
flavor.signingConfig signingConfigs.publishing
}
productFlavors.freehuawei.signingConfig signingConfigs.publishingHuawei
}
}
@ -276,44 +286,12 @@ task downloadWorldMiniBasemap {
}
}
task downloadHuaweiDrmZip {
task setupHuaweiConfig {
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')
ant.unzip(src: 'HwDRM_SDK_2.5.2.300_ADT.zip', dest: 'huaweidrmlib/')
if (System.getenv("HUAWEI_SDK_JSON")) {
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) {
@ -397,8 +375,6 @@ task copyLargePOIIcons(type: Sync) {
}
}
task copyWidgetIconsXhdpi(type: Sync) {
from "res/drawable-xxhdpi/"
into "res/drawable-large-xhdpi/"
@ -445,13 +421,9 @@ task collectExternalResources {
copyPoiCategories,
downloadWorldMiniBasemap
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString().toLowerCase()
// Use Drm SDK only for huawei build
String tskReqStr = gradle.startParameter.taskNames.toString()
if (tskReqStr.contains("huawei")) {
dependsOn downloadPrebuiltHuaweiDrm
} else {
dependsOn cleanPrebuiltHuaweiDrm
dependsOn setupHuaweiConfig
}
}
@ -503,10 +475,16 @@ task cleanupDuplicatesInCore() {
file("libs/x86_64/libc++_shared.so").renameTo(file("libc++/x86_64/libc++_shared.so"))
}
}
afterEvaluate {
android.applicationVariants.all { variant ->
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) {
@ -516,7 +494,6 @@ task appStart(type: Exec) {
// commandLine 'cmd', '/c', 'adb', 'shell', 'am', 'start', '-n', 'net.osmand.plus/net.osmand.plus.activities.MapActivity'
}
dependencies {
implementation project(path: ':OsmAnd-java', configuration: 'android')
implementation project(':OsmAnd-api')
@ -565,6 +542,5 @@ dependencies {
}
implementation 'com.jaredrummler:colorpicker:1.1.0'
huaweiImplementation files('libs/huawei-android-drm_v2.5.2.300.jar')
freehuaweiImplementation files('libs/huawei-android-drm_v2.5.2.300.jar')
freehuaweiImplementation 'com.huawei.hms:iap:5.0.2.300'
}

View file

@ -11,6 +11,7 @@
Thx - Hardy
-->
<string name="contour_lines_thanks">Thank you for purchasing \'Contour lines\'</string>
<string name="start_finish_icons">Start/finish icons</string>
<string name="sort_name_ascending">Name: A Z</string>
<string name="sort_name_descending">Name: Z A</string>

View file

@ -0,0 +1,583 @@
package net.osmand.plus.inapp;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.widget.Toast;
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.OsmandPlugin;
import net.osmand.plus.R;
import net.osmand.plus.inapp.InAppPurchases.InAppPurchase;
import net.osmand.plus.inapp.InAppPurchases.InAppSubscription;
import net.osmand.plus.inapp.InAppPurchasesImpl.InAppPurchaseLiveUpdatesOldSubscription;
import net.osmand.plus.inapp.util.BillingManager;
import net.osmand.plus.settings.backend.OsmandSettings;
import net.osmand.plus.srtmplugin.SRTMPlugin;
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);
purchases = new InAppPurchasesImpl(ctx);
}
@Override
public void isInAppPurchaseSupported(@NonNull final Activity activity, @Nullable final InAppPurchaseInitCallback callback) {
if (callback != null) {
callback.onSuccess();
}
}
private BillingManager getBillingManager() {
return billingManager;
}
protected void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppCommand 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;
}
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 (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 (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(@NonNull final Activity activity) {
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION);
exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, new InAppCommand() {
@Override
public void 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");
}
commandDone();
} catch (Exception e) {
complain("Cannot launch full version purchase!");
logError("purchaseFullVersion Error", e);
stop(true);
}
}
});
}
@Override
public void purchaseDepthContours(@NonNull final Activity activity) {
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS);
exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, new InAppCommand() {
@Override
public void 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");
}
commandDone();
} catch (Exception e) {
complain("Cannot launch depth contours purchase!");
logError("purchaseDepthContours Error", e);
stop(true);
}
}
});
}
@Override
public void purchaseContourLines(@NonNull Activity activity) throws UnsupportedOperationException {
OsmandPlugin plugin = OsmandPlugin.getPlugin(SRTMPlugin.class);
if(plugin == null || plugin.getInstallURL() == null) {
Toast.makeText(activity.getApplicationContext(),
activity.getString(R.string.activate_srtm_plugin), Toast.LENGTH_LONG).show();
} else {
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(plugin.getInstallURL())));
}
}
@Override
public void manageSubscription(@NonNull Context ctx, @Nullable String sku) {
String url = "https://play.google.com/store/account/subscriptions?package=" + ctx.getPackageName();
if (!Algorithms.isEmpty(sku)) {
url += "&sku=" + sku;
}
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
ctx.startActivity(intent);
}
@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 (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;
}
}
}
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 InAppPurchase inAppPurchase, @NonNull SkuDetails skuDetails, @Nullable Purchase purchase) {
if (purchase != null) {
inAppPurchase.setPurchaseState(InAppPurchase.PurchaseState.PURCHASED);
inAppPurchase.setPurchaseTime(purchase.getPurchaseTime());
} else {
inAppPurchase.setPurchaseState(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 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 InAppPurchases.InAppSubscriptionIntroductoryInfo(s, introductoryPrice,
introductoryPriceAmountMicros, introductoryPricePeriod, introductoryPriceCycles));
} catch (ParseException e) {
LOG.error(e);
}
}
}
}
protected InAppCommand getPurchaseLiveUpdatesCommand(final WeakReference<Activity> activity, final String sku, final String payload) {
return new InAppCommand() {
@Override
public void 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");
}
commandDone();
} else {
stop(true);
}
} catch (Exception e) {
logError("launchPurchaseFlow Error", e);
stop(true);
}
}
};
}
protected InAppCommand getRequestInventoryCommand() {
return new InAppCommand() {
@Override
public void run(InAppPurchaseHelper helper) {
logDebug("Setup successful. Querying inventory.");
try {
BillingManager billingManager = getBillingManager();
if (billingManager != null) {
billingManager.queryPurchases();
} else {
throw new IllegalStateException("BillingManager disposed");
}
commandDone();
} catch (Exception e) {
logError("queryInventoryAsync Error", e);
notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY);
stop(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;
}
}
}

View file

@ -0,0 +1,323 @@
package net.osmand.plus.inapp;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.billingclient.api.SkuDetails;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.R;
import net.osmand.plus.Version;
public class InAppPurchasesImpl extends InAppPurchases {
private static final InAppPurchase FULL_VERSION = new InAppPurchaseFullVersion();
private static final InAppPurchaseDepthContoursFull DEPTH_CONTOURS_FULL = new InAppPurchaseDepthContoursFull();
private static final InAppPurchaseDepthContoursFree DEPTH_CONTOURS_FREE = new InAppPurchaseDepthContoursFree();
private static final InAppPurchaseContourLinesFull CONTOUR_LINES_FULL = new InAppPurchaseContourLinesFull();
private static final InAppPurchaseContourLinesFree CONTOUR_LINES_FREE = new InAppPurchaseContourLinesFree();
private static final InAppSubscription[] LIVE_UPDATES_FULL = new InAppSubscription[]{
new InAppPurchaseLiveUpdatesOldMonthlyFull(),
new InAppPurchaseLiveUpdatesMonthlyFull(),
new InAppPurchaseLiveUpdates3MonthsFull(),
new InAppPurchaseLiveUpdatesAnnualFull()
};
private static final InAppSubscription[] LIVE_UPDATES_FREE = new InAppSubscription[]{
new InAppPurchaseLiveUpdatesOldMonthlyFree(),
new InAppPurchaseLiveUpdatesMonthlyFree(),
new InAppPurchaseLiveUpdates3MonthsFree(),
new InAppPurchaseLiveUpdatesAnnualFree()
};
public InAppPurchasesImpl(OsmandApplication ctx) {
super(ctx);
fullVersion = FULL_VERSION;
if (Version.isFreeVersion(ctx)) {
liveUpdates = new LiveUpdatesInAppPurchasesFree();
} else {
liveUpdates = new LiveUpdatesInAppPurchasesFull();
}
for (InAppSubscription s : liveUpdates.getAllSubscriptions()) {
if (s instanceof InAppPurchaseLiveUpdatesMonthly) {
if (s.isDiscounted()) {
discountedMonthlyLiveUpdates = s;
} else {
monthlyLiveUpdates = s;
}
}
}
if (Version.isFreeVersion(ctx)) {
depthContours = DEPTH_CONTOURS_FREE;
} else {
depthContours = DEPTH_CONTOURS_FULL;
}
if (Version.isFreeVersion(ctx)) {
contourLines = CONTOUR_LINES_FREE;
} else {
contourLines = CONTOUR_LINES_FULL;
}
inAppPurchases = new InAppPurchase[] { fullVersion, depthContours, contourLines };
}
@Override
public boolean isFullVersion(String sku) {
return FULL_VERSION.getSku().equals(sku);
}
@Override
public boolean isDepthContours(String sku) {
return DEPTH_CONTOURS_FULL.getSku().equals(sku) || DEPTH_CONTOURS_FREE.getSku().equals(sku);
}
@Override
public boolean isContourLines(String sku) {
return CONTOUR_LINES_FULL.getSku().equals(sku) || CONTOUR_LINES_FREE.getSku().equals(sku);
}
@Override
public boolean isLiveUpdates(String sku) {
for (InAppPurchase p : LIVE_UPDATES_FULL) {
if (p.getSku().equals(sku)) {
return true;
}
}
for (InAppPurchase p : LIVE_UPDATES_FREE) {
if (p.getSku().equals(sku)) {
return true;
}
}
return false;
}
private static class InAppPurchaseFullVersion extends InAppPurchase {
private static final String SKU_FULL_VERSION_PRICE = "osmand_full_version_price";
InAppPurchaseFullVersion() {
super(SKU_FULL_VERSION_PRICE);
}
@Override
public String getDefaultPrice(Context ctx) {
return ctx.getString(R.string.full_version_price);
}
}
private static class InAppPurchaseDepthContoursFull extends InAppPurchaseDepthContours {
private static final String SKU_DEPTH_CONTOURS_FULL = "net.osmand.seadepth_plus";
InAppPurchaseDepthContoursFull() {
super(SKU_DEPTH_CONTOURS_FULL);
}
}
private static class InAppPurchaseDepthContoursFree extends InAppPurchaseDepthContours {
private static final String SKU_DEPTH_CONTOURS_FREE = "net.osmand.seadepth";
InAppPurchaseDepthContoursFree() {
super(SKU_DEPTH_CONTOURS_FREE);
}
}
private static class InAppPurchaseContourLinesFull extends InAppPurchaseContourLines {
private static final String SKU_CONTOUR_LINES_FULL = "net.osmand.contourlines_plus";
InAppPurchaseContourLinesFull() {
super(SKU_CONTOUR_LINES_FULL);
}
}
private static class InAppPurchaseContourLinesFree extends InAppPurchaseContourLines {
private static final String SKU_CONTOUR_LINES_FREE = "net.osmand.contourlines";
InAppPurchaseContourLinesFree() {
super(SKU_CONTOUR_LINES_FREE);
}
}
private static class InAppPurchaseLiveUpdatesMonthlyFull extends InAppPurchaseLiveUpdatesMonthly {
private static final String SKU_LIVE_UPDATES_MONTHLY_FULL = "osm_live_subscription_monthly_full";
InAppPurchaseLiveUpdatesMonthlyFull() {
super(SKU_LIVE_UPDATES_MONTHLY_FULL, 1);
}
private InAppPurchaseLiveUpdatesMonthlyFull(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFull(sku) : null;
}
}
private static class InAppPurchaseLiveUpdatesMonthlyFree extends InAppPurchaseLiveUpdatesMonthly {
private static final String SKU_LIVE_UPDATES_MONTHLY_FREE = "osm_live_subscription_monthly_free";
InAppPurchaseLiveUpdatesMonthlyFree() {
super(SKU_LIVE_UPDATES_MONTHLY_FREE, 1);
}
private InAppPurchaseLiveUpdatesMonthlyFree(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFree(sku) : null;
}
}
private static class InAppPurchaseLiveUpdates3MonthsFull extends InAppPurchaseLiveUpdates3Months {
private static final String SKU_LIVE_UPDATES_3_MONTHS_FULL = "osm_live_subscription_3_months_full";
InAppPurchaseLiveUpdates3MonthsFull() {
super(SKU_LIVE_UPDATES_3_MONTHS_FULL, 1);
}
private InAppPurchaseLiveUpdates3MonthsFull(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFull(sku) : null;
}
}
private static class InAppPurchaseLiveUpdates3MonthsFree extends InAppPurchaseLiveUpdates3Months {
private static final String SKU_LIVE_UPDATES_3_MONTHS_FREE = "osm_live_subscription_3_months_free";
InAppPurchaseLiveUpdates3MonthsFree() {
super(SKU_LIVE_UPDATES_3_MONTHS_FREE, 1);
}
private InAppPurchaseLiveUpdates3MonthsFree(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFree(sku) : null;
}
}
private static class InAppPurchaseLiveUpdatesAnnualFull extends InAppPurchaseLiveUpdatesAnnual {
private static final String SKU_LIVE_UPDATES_ANNUAL_FULL = "osm_live_subscription_annual_full";
InAppPurchaseLiveUpdatesAnnualFull() {
super(SKU_LIVE_UPDATES_ANNUAL_FULL, 1);
}
private InAppPurchaseLiveUpdatesAnnualFull(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFull(sku) : null;
}
}
private static class InAppPurchaseLiveUpdatesAnnualFree extends InAppPurchaseLiveUpdatesAnnual {
private static final String SKU_LIVE_UPDATES_ANNUAL_FREE = "osm_live_subscription_annual_free";
InAppPurchaseLiveUpdatesAnnualFree() {
super(SKU_LIVE_UPDATES_ANNUAL_FREE, 1);
}
private InAppPurchaseLiveUpdatesAnnualFree(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFree(sku) : null;
}
}
private static class InAppPurchaseLiveUpdatesOldMonthlyFull extends InAppPurchaseLiveUpdatesOldMonthly {
private static final String SKU_LIVE_UPDATES_OLD_MONTHLY_FULL = "osm_live_subscription_2";
InAppPurchaseLiveUpdatesOldMonthlyFull() {
super(SKU_LIVE_UPDATES_OLD_MONTHLY_FULL);
}
}
private static class InAppPurchaseLiveUpdatesOldMonthlyFree extends InAppPurchaseLiveUpdatesOldMonthly {
private static final String SKU_LIVE_UPDATES_OLD_MONTHLY_FREE = "osm_free_live_subscription_2";
InAppPurchaseLiveUpdatesOldMonthlyFree() {
super(SKU_LIVE_UPDATES_OLD_MONTHLY_FREE);
}
}
public static class InAppPurchaseLiveUpdatesOldSubscription extends InAppSubscription {
private SkuDetails details;
InAppPurchaseLiveUpdatesOldSubscription(@NonNull SkuDetails details) {
super(details.getSku(), true);
this.details = details;
}
@Override
public String getDefaultPrice(Context ctx) {
return "";
}
@Override
public CharSequence getTitle(Context ctx) {
return details.getTitle();
}
@Override
public CharSequence getDescription(@NonNull Context ctx) {
return details.getDescription();
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return null;
}
}
private static class LiveUpdatesInAppPurchasesFree extends InAppSubscriptionList {
public LiveUpdatesInAppPurchasesFree() {
super(LIVE_UPDATES_FREE);
}
}
private static class LiveUpdatesInAppPurchasesFull extends InAppSubscriptionList {
public LiveUpdatesInAppPurchasesFull() {
super(LIVE_UPDATES_FULL);
}
}
}

View file

@ -0,0 +1,96 @@
/**
* Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
/**
* Signature related tools.
*
* @since 2019/12/9
*/
public class CipherUtil {
private static final String TAG = "CipherUtil";
private static final String SIGN_ALGORITHMS = "SHA256WithRSA";
private static final String PUBLIC_KEY = "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAsB+oH8rYQncwpTqGa0kS/5E725HJrq2sW1ThAZtmorYVi52Yt9PmZvNDz7284ol9C2skrKQR34eIer8Tr7Qqq3mlNo+/LVUpq9sa++kB2glaG6jj5NNjM3w4nVYHFIYkd5AQhodJgmqFvnp2s7r7YmyQVXZSehei5bA1G70Bs+El9cSv9shNNGTCaU3ARUu2hy3Ltkc/ov7/ZYYpiwjbyD3cmoMh9jO1++zztXb2phjv1h9zeJOp1i6HsotZll+c9J4jjV3GhrF+ZJm5WrSzGLDLtwSldRpMZFxrSvAJJstjzhDz3LpUM+nPV3HZ5VQ/xosmwWYmiibo89E1gw8p73NTBXHzuQMJcTJ6vTjD8LeMskpXHZUAGhifmFLGN1LbNP9662ulCV12kIbXuzWCwwi/h0DWqmnjKmLvzc88e4BrGrp2zZUnHz7m15voPG+4cQ3z9+cwS4gEI3SUTiFyQGE539SO/11VkkQAJ8P7du1JFNqQw5ZEW3AoE1iUsp5XAgMBAAE=";
/**
* the method to check the signature for the data returned from the interface
* @param content Unsigned data
* @param sign the signature for content
* @param publicKey the public of the application
* @return boolean
*/
public static boolean doCheck(String content, String sign, String publicKey) {
if (TextUtils.isEmpty(publicKey)) {
Log.e(TAG, "publicKey is null");
return false;
}
if (TextUtils.isEmpty(content) || TextUtils.isEmpty(sign)) {
Log.e(TAG, "data is error");
return false;
}
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] encodedKey = Base64.decode(publicKey, Base64.DEFAULT);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS);
signature.initVerify(pubKey);
signature.update(content.getBytes("UTF-8"));
boolean bverify = signature.verify(Base64.decode(sign, Base64.DEFAULT));
return bverify;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "doCheck NoSuchAlgorithmException" + e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "doCheck InvalidKeySpecException" + e);
} catch (InvalidKeyException e) {
Log.e(TAG, "doCheck InvalidKeyException" + e);
} catch (SignatureException e) {
Log.e(TAG, "doCheck SignatureException" + e);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "doCheck UnsupportedEncodingException" + e);
}
return false;
}
/**
* get the publicKey of the application
* During the encoding process, avoid storing the public key in clear text.
* @return publickey
*/
public static String getPublicKey(){
return PUBLIC_KEY;
}
}

View file

@ -0,0 +1,33 @@
/**
* Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp;
/**
* Constants Class.
*
* @since 2019/12/9
*/
public class Constants {
/** requestCode for pull up the pmsPay page */
public static final int REQ_CODE_BUY_SUB = 4002;
public static final int REQ_CODE_BUY_INAPP = 4003;
/** requestCode for pull up the login page for isEnvReady interface */
public static final int REQ_CODE_LOGIN = 2001;
}

View file

@ -0,0 +1,103 @@
/**
* Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp;
import android.app.Activity;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.huawei.hms.iap.IapApiException;
import com.huawei.hms.iap.entity.OrderStatusCode;
import net.osmand.AndroidUtils;
import net.osmand.PlatformUtil;
/**
* Handles the exception returned from the iap api.
*
* @since 2019/12/9
*/
public class ExceptionHandle {
protected static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(ExceptionHandle.class);
/**
* The exception is solved.
*/
public static final int SOLVED = 0;
/**
* Handles the exception returned from the iap api.
* @param activity The Activity to call the iap api.
* @param e The exception returned from the iap api.
* @return int
*/
public static int handle(@Nullable Activity activity, Exception e) {
if (e instanceof IapApiException) {
IapApiException iapApiException = (IapApiException) e;
LOG.info("returnCode: " + iapApiException.getStatusCode());
switch (iapApiException.getStatusCode()) {
case OrderStatusCode.ORDER_STATE_CANCEL:
showToast(activity, "Order has been canceled!");
return SOLVED;
case OrderStatusCode.ORDER_STATE_PARAM_ERROR:
showToast(activity, "Order state param error!");
return SOLVED;
case OrderStatusCode.ORDER_STATE_NET_ERROR:
showToast(activity, "Order state net error!");
return SOLVED;
case OrderStatusCode.ORDER_VR_UNINSTALL_ERROR:
showToast(activity, "Order vr uninstall error!");
return SOLVED;
case OrderStatusCode.ORDER_HWID_NOT_LOGIN:
IapRequestHelper.startResolutionForResult(activity, iapApiException.getStatus(), Constants.REQ_CODE_LOGIN);
return SOLVED;
case OrderStatusCode.ORDER_PRODUCT_OWNED:
showToast(activity, "Product already owned error!");
return OrderStatusCode.ORDER_PRODUCT_OWNED;
case OrderStatusCode.ORDER_PRODUCT_NOT_OWNED:
showToast(activity, "Product not owned error!");
return SOLVED;
case OrderStatusCode.ORDER_PRODUCT_CONSUMED:
showToast(activity, "Product consumed error!");
return SOLVED;
case OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED:
showToast(activity, "Order account area not supported error!");
return SOLVED;
case OrderStatusCode.ORDER_NOT_ACCEPT_AGREEMENT:
showToast(activity, "User does not agree the agreement");
return SOLVED;
default:
// handle other error scenarios
showToast(activity, "Order unknown error!");
return SOLVED;
}
} else {
showToast(activity, "External error");
LOG.error(e.getMessage(), e);
return SOLVED;
}
}
private static void showToast(@Nullable Activity activity, String s) {
if (AndroidUtils.isActivityNotDestroyed(activity)) {
Toast.makeText(activity, s, Toast.LENGTH_SHORT).show();
}
}
}

View file

@ -0,0 +1,37 @@
/**
* Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp;
/**
* Used to callback the result from iap api.
*
* @since 2019/12/9
*/
public interface IapApiCallback<T> {
/**
* The request is successful.
* @param result The result of a successful response.
*/
void onSuccess(T result);
/**
* Callback fail.
* @param e An Exception from IAPSDK.
*/
void onFail(Exception e);
}

View file

@ -0,0 +1,351 @@
/**
* Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp;
import android.app.Activity;
import android.content.IntentSender;
import android.text.TextUtils;
import android.util.Log;
import com.huawei.hmf.tasks.OnFailureListener;
import com.huawei.hmf.tasks.OnSuccessListener;
import com.huawei.hmf.tasks.Task;
import com.huawei.hms.iap.Iap;
import com.huawei.hms.iap.IapApiException;
import com.huawei.hms.iap.IapClient;
import com.huawei.hms.iap.entity.ConsumeOwnedPurchaseReq;
import com.huawei.hms.iap.entity.ConsumeOwnedPurchaseResult;
import com.huawei.hms.iap.entity.IsEnvReadyResult;
import com.huawei.hms.iap.entity.OwnedPurchasesReq;
import com.huawei.hms.iap.entity.OwnedPurchasesResult;
import com.huawei.hms.iap.entity.ProductInfoReq;
import com.huawei.hms.iap.entity.ProductInfoResult;
import com.huawei.hms.iap.entity.PurchaseIntentReq;
import com.huawei.hms.iap.entity.PurchaseIntentResult;
import com.huawei.hms.iap.entity.StartIapActivityReq;
import com.huawei.hms.iap.entity.StartIapActivityResult;
import com.huawei.hms.support.api.client.Status;
import java.util.List;
/**
* The tool class of Iap interface.
*
* @since 2019/12/9
*/
public class IapRequestHelper {
private final static String TAG = "IapRequestHelper";
/**
* Create a PurchaseIntentReq object.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
* @param productId ID of the in-app product to be paid.
* The in-app product ID is the product ID you set during in-app product configuration in AppGallery Connect.
* @return PurchaseIntentReq
*/
private static PurchaseIntentReq createPurchaseIntentReq(int type, String productId) {
PurchaseIntentReq req = new PurchaseIntentReq();
req.setPriceType(type);
req.setProductId(productId);
req.setDeveloperPayload("testPurchase");
return req;
}
/**
* Create a ConsumeOwnedPurchaseReq object.
* @param purchaseToken which is generated by the Huawei payment server during product payment and returned to the app through InAppPurchaseData.
* The app transfers this parameter for the Huawei payment server to update the order status and then deliver the in-app product.
* @return ConsumeOwnedPurchaseReq
*/
private static ConsumeOwnedPurchaseReq createConsumeOwnedPurchaseReq(String purchaseToken) {
ConsumeOwnedPurchaseReq req = new ConsumeOwnedPurchaseReq();
req.setPurchaseToken(purchaseToken);
req.setDeveloperChallenge("testConsume");
return req;
}
/**
* Create a OwnedPurchasesReq object.
* @param type type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
* @param continuationToken A data location flag which returns from obtainOwnedPurchases api or obtainOwnedPurchaseRecord api.
* @return OwnedPurchasesReq
*/
private static OwnedPurchasesReq createOwnedPurchasesReq(int type, String continuationToken) {
OwnedPurchasesReq req = new OwnedPurchasesReq();
req.setPriceType(type);
req.setContinuationToken(continuationToken);
return req;
}
/**
* Create a ProductInfoReq object.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
* @param productIds ID list of products to be queried. Each product ID must exist and be unique in the current app.
* @return ProductInfoReq
*/
private static ProductInfoReq createProductInfoReq(int type, List<String> productIds) {
ProductInfoReq req = new ProductInfoReq();
req.setPriceType(type);
req.setProductIds(productIds);
return req;
}
/**
* To check whether the country or region of the logged in HUAWEI ID is included in the countries or regions supported by HUAWEI IAP.
* @param mClient IapClient instance to call the isEnvReady API.
* @param callback IapApiCallback.
*/
public static void isEnvReady(IapClient mClient, final IapApiCallback callback) {
Log.i(TAG, "call isEnvReady");
Task<IsEnvReadyResult> task = mClient.isEnvReady();
task.addOnSuccessListener(new OnSuccessListener<IsEnvReadyResult>() {
@Override
public void onSuccess(IsEnvReadyResult result) {
Log.i(TAG, "isEnvReady, success");
callback.onSuccess(result);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, "isEnvReady, fail");
callback.onFail(e);
}
});
}
/**
* Obtain in-app product details configured in AppGallery Connect.
* @param iapClient IapClient instance to call the obtainProductInfo API.
* @param productIds ID list of products to be queried. Each product ID must exist and be unique in the current app.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
* @param callback IapApiCallback
*/
public static void obtainProductInfo(IapClient iapClient, final List<String> productIds, int type, final IapApiCallback callback) {
Log.i(TAG, "call obtainProductInfo");
Task<ProductInfoResult> task = iapClient.obtainProductInfo(createProductInfoReq(type, productIds));
task.addOnSuccessListener(new OnSuccessListener<ProductInfoResult>() {
@Override
public void onSuccess(ProductInfoResult result) {
Log.i(TAG, "obtainProductInfo, success");
callback.onSuccess(result);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, "obtainProductInfo, fail");
callback.onFail(e);
}
});
}
/**
* create orders for in-app products in the PMS
* @param iapClient IapClient instance to call the createPurchaseIntent API.
* @param productId ID of the in-app product to be paid.
* The in-app product ID is the product ID you set during in-app product configuration in AppGallery Connect.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
* @param callback IapApiCallback
*/
public static void createPurchaseIntent(final IapClient iapClient, String productId, int type, final IapApiCallback callback) {
Log.i(TAG, "call createPurchaseIntent");
Task<PurchaseIntentResult> task = iapClient.createPurchaseIntent(createPurchaseIntentReq(type, productId));
task.addOnSuccessListener(new OnSuccessListener<PurchaseIntentResult>() {
@Override
public void onSuccess(PurchaseIntentResult result) {
Log.i(TAG, "createPurchaseIntent, success");
callback.onSuccess(result);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, "createPurchaseIntent, fail");
callback.onFail(e);
}
});
}
public static void createPurchaseIntent(final IapClient iapClient, String productId, int type, String payload, final IapApiCallback callback) {
Log.i(TAG, "call createPurchaseIntent");
PurchaseIntentReq req = createPurchaseIntentReq(type, productId);
req.setDeveloperPayload(payload);
Task<PurchaseIntentResult> task = iapClient.createPurchaseIntent(req);
task.addOnSuccessListener(new OnSuccessListener<PurchaseIntentResult>() {
@Override
public void onSuccess(PurchaseIntentResult result) {
Log.i(TAG, "createPurchaseIntent, success");
callback.onSuccess(result);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, "createPurchaseIntent, fail");
callback.onFail(e);
}
});
}
/**
* to start an activity.
* @param activity the activity to launch a new page.
* @param status This parameter contains the pendingIntent object of the payment page.
* @param reqCode Result code.
*/
public static void startResolutionForResult(Activity activity, Status status, int reqCode) {
if (status == null) {
Log.e(TAG, "status is null");
return;
}
if (status.hasResolution()) {
try {
status.startResolutionForResult(activity, reqCode);
} catch (IntentSender.SendIntentException exp) {
Log.e(TAG, exp.getMessage());
}
} else {
Log.e(TAG, "intent is null");
}
}
/**
* query information about all subscribed in-app products, including consumables, non-consumables, and auto-renewable subscriptions.</br>
* If consumables are returned, the system needs to deliver them and calls the consumeOwnedPurchase API to consume the products.
* If non-consumables are returned, the in-app products do not need to be consumed.
* If subscriptions are returned, all existing subscription relationships of the user under the app are returned.
* @param mClient IapClient instance to call the obtainOwnedPurchases API.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
* @param callback IapApiCallback
*/
public static void obtainOwnedPurchases(IapClient mClient, final int type, String continuationToken, final IapApiCallback callback) {
Log.i(TAG, "call obtainOwnedPurchases");
Task<OwnedPurchasesResult> task = mClient.obtainOwnedPurchases(IapRequestHelper.createOwnedPurchasesReq(type, continuationToken));
task.addOnSuccessListener(new OnSuccessListener<OwnedPurchasesResult>() {
@Override
public void onSuccess(OwnedPurchasesResult result) {
Log.i(TAG, "obtainOwnedPurchases, success");
callback.onSuccess(result);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, "obtainOwnedPurchases, fail");
callback.onFail(e);
}
});
}
/**
* obtain the historical consumption information about a consumable in-app product or all subscription receipts of a subscription.
* @param iapClient IapClient instance to call the obtainOwnedPurchaseRecord API.
* @param priceType In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription.
* @param continuationToken Data locating flag for supporting query in pagination mode.
* @param callback IapApiCallback
*/
public static void obtainOwnedPurchaseRecord(IapClient iapClient, int priceType, String continuationToken, final IapApiCallback callback) {
Log.i(TAG, "call obtainOwnedPurchaseRecord");
Task<OwnedPurchasesResult> task = iapClient.obtainOwnedPurchaseRecord(createOwnedPurchasesReq(priceType, continuationToken));
task.addOnSuccessListener(new OnSuccessListener<OwnedPurchasesResult>() {
@Override
public void onSuccess(OwnedPurchasesResult result) {
Log.i(TAG, "obtainOwnedPurchaseRecord, success");
callback.onSuccess(result);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, "obtainOwnedPurchaseRecord, fail");
callback.onFail(e);
}
});
}
/**
* Consume all the unconsumed purchases with priceType 0.
* @param iapClient IapClient instance to call the consumeOwnedPurchase API.
* @param purchaseToken which is generated by the Huawei payment server during product payment and returned to the app through InAppPurchaseData.
*/
public static void consumeOwnedPurchase(IapClient iapClient, String purchaseToken) {
Log.i(TAG, "call consumeOwnedPurchase");
Task<ConsumeOwnedPurchaseResult> task = iapClient.consumeOwnedPurchase(createConsumeOwnedPurchaseReq(purchaseToken));
task.addOnSuccessListener(new OnSuccessListener<ConsumeOwnedPurchaseResult>() {
@Override
public void onSuccess(ConsumeOwnedPurchaseResult result) {
// Consume success.
Log.i(TAG, "consumeOwnedPurchase success");
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException)e;
int returnCode = apiException.getStatusCode();
Log.e(TAG, "consumeOwnedPurchase fail, IapApiException returnCode: " + returnCode);
} else {
// Other external errors
Log.e(TAG, e.getMessage());
}
}
});
}
/**
* link to subscription manager page
* @param activity activity
* @param productId the productId of the subscription product
*/
public static void showSubscription(final Activity activity, String productId) {
StartIapActivityReq req = new StartIapActivityReq();
if (TextUtils.isEmpty(productId)) {
req.setType(StartIapActivityReq.TYPE_SUBSCRIBE_MANAGER_ACTIVITY);
} else {
req.setType(StartIapActivityReq.TYPE_SUBSCRIBE_EDIT_ACTIVITY);
req.setSubscribeProductId(productId);
}
IapClient iapClient = Iap.getIapClient(activity);
Task<StartIapActivityResult> task = iapClient.startIapActivity(req);
task.addOnSuccessListener(new OnSuccessListener<StartIapActivityResult>() {
@Override
public void onSuccess(StartIapActivityResult result) {
if(result != null) {
result.startActivity(activity);
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
ExceptionHandle.handle(activity, e);
}
});
}
}

View file

@ -0,0 +1,703 @@
package net.osmand.plus.inapp;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.huawei.hmf.tasks.OnFailureListener;
import com.huawei.hmf.tasks.OnSuccessListener;
import com.huawei.hmf.tasks.Task;
import com.huawei.hms.iap.Iap;
import com.huawei.hms.iap.IapClient;
import com.huawei.hms.iap.entity.InAppPurchaseData;
import com.huawei.hms.iap.entity.IsEnvReadyResult;
import com.huawei.hms.iap.entity.OrderStatusCode;
import com.huawei.hms.iap.entity.OwnedPurchasesResult;
import com.huawei.hms.iap.entity.ProductInfo;
import com.huawei.hms.iap.entity.ProductInfoResult;
import com.huawei.hms.iap.entity.PurchaseIntentResult;
import com.huawei.hms.iap.entity.PurchaseResultInfo;
import com.huawei.hms.iap.entity.StartIapActivityReq;
import com.huawei.hms.iap.entity.StartIapActivityResult;
import net.osmand.AndroidUtils;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.inapp.InAppPurchases.InAppPurchase;
import net.osmand.plus.inapp.InAppPurchases.InAppSubscription;
import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionIntroductoryInfo;
import net.osmand.plus.inapp.InAppPurchasesImpl.InAppPurchaseLiveUpdatesOldSubscription;
import net.osmand.plus.settings.backend.OsmandSettings;
import net.osmand.plus.settings.backend.OsmandSettings.OsmandPreference;
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.HashSet;
import java.util.List;
import java.util.Set;
public class InAppPurchaseHelperImpl extends InAppPurchaseHelper {
private boolean envReady = false;
private boolean purchaseSupported = false;
private List<ProductInfo> productInfos;
private OwnedPurchasesResult ownedSubscriptions;
private List<OwnedPurchasesResult> ownedInApps = new ArrayList<>();
public InAppPurchaseHelperImpl(OsmandApplication ctx) {
super(ctx);
purchases = new InAppPurchasesImpl(ctx);
}
@Override
public void isInAppPurchaseSupported(@NonNull final Activity activity, @Nullable final InAppPurchaseInitCallback callback) {
if (envReady) {
if (callback != null) {
if (purchaseSupported) {
callback.onSuccess();
} else {
callback.onFail();
}
}
} else {
// Initiating an isEnvReady request when entering the app.
// Check if the account service country supports IAP.
IapClient mClient = Iap.getIapClient(activity);
final WeakReference<Activity> activityRef = new WeakReference<>(activity);
IapRequestHelper.isEnvReady(mClient, new IapApiCallback<IsEnvReadyResult>() {
private void onReady(boolean succeed) {
logDebug("Setup finished.");
envReady = true;
purchaseSupported = succeed;
if (callback != null) {
if (succeed) {
callback.onSuccess();
} else {
callback.onFail();
}
}
}
@Override
public void onSuccess(IsEnvReadyResult result) {
onReady(true);
}
@Override
public void onFail(Exception e) {
onReady(false);
LOG.error("isEnvReady fail, " + e.getMessage(), e);
ExceptionHandle.handle(activityRef.get(), e);
}
});
}
}
protected void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppCommand command) {
if (envReady) {
command.run(this);
} else {
command.commandDone();
}
}
private InAppCommand getPurchaseInAppCommand(@NonNull final Activity activity, @NonNull final String productId) throws UnsupportedOperationException {
return new InAppCommand() {
@Override
public void run(InAppPurchaseHelper helper) {
try {
ProductInfo productInfo = getProductInfo(productId);
if (productInfo != null) {
IapRequestHelper.createPurchaseIntent(getIapClient(), productInfo.getProductId(),
IapClient.PriceType.IN_APP_NONCONSUMABLE, new IapApiCallback<PurchaseIntentResult>() {
@Override
public void onSuccess(PurchaseIntentResult result) {
if (result == null) {
logError("result is null");
} else {
// you should pull up the page to complete the payment process
IapRequestHelper.startResolutionForResult(activity, result.getStatus(), Constants.REQ_CODE_BUY_INAPP);
}
commandDone();
}
@Override
public void onFail(Exception e) {
int errorCode = ExceptionHandle.handle(activity, e);
if (errorCode != ExceptionHandle.SOLVED) {
logDebug("createPurchaseIntent, returnCode: " + errorCode);
if (OrderStatusCode.ORDER_PRODUCT_OWNED == errorCode) {
logError("already own this product");
} else {
logError("unknown error");
}
}
commandDone();
}
});
} else {
commandDone();
}
} catch (Exception e) {
complain("Cannot launch full version purchase!");
logError("purchaseFullVersion Error", e);
stop(true);
}
}
};
}
@Override
public void purchaseFullVersion(@NonNull final Activity activity) throws UnsupportedOperationException {
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION);
exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, getPurchaseInAppCommand(activity, purchases.getFullVersion().getSku()));
}
@Override
public void purchaseDepthContours(@NonNull final Activity activity) throws UnsupportedOperationException {
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS);
exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, getPurchaseInAppCommand(activity, purchases.getDepthContours().getSku()));
}
@Override
public void purchaseContourLines(@NonNull Activity activity) throws UnsupportedOperationException {
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_CONTOUR_LINES);
exec(InAppPurchaseTaskType.PURCHASE_CONTOUR_LINES, getPurchaseInAppCommand(activity, purchases.getContourLines().getSku()));
}
@Override
public void manageSubscription(@NonNull Context ctx, @Nullable String sku) {
if (uiActivity != null) {
StartIapActivityReq req = new StartIapActivityReq();
if (!Algorithms.isEmpty(sku)) {
req.setSubscribeProductId(sku);
req.setType(StartIapActivityReq.TYPE_SUBSCRIBE_EDIT_ACTIVITY);
} else {
req.setType(StartIapActivityReq.TYPE_SUBSCRIBE_MANAGER_ACTIVITY);
}
Task<StartIapActivityResult> task = getIapClient().startIapActivity(req);
task.addOnSuccessListener(new OnSuccessListener<StartIapActivityResult>() {
@Override
public void onSuccess(StartIapActivityResult result) {
logDebug("startIapActivity: onSuccess");
Activity activity = (Activity) uiActivity;
if (result != null && AndroidUtils.isActivityNotDestroyed(activity)) {
result.startActivity(activity);
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
logDebug("startIapActivity: onFailure");
}
});
}
}
@Nullable
private ProductInfo getProductInfo(@NonNull String productId) {
List<ProductInfo> productInfos = this.productInfos;
if (productInfos != null) {
for (ProductInfo info : productInfos) {
if (info.getProductId().equals(productId)) {
return info;
}
}
}
return null;
}
private boolean hasDetails(@NonNull String productId) {
return getProductInfo(productId) != null;
}
@Nullable
private InAppPurchaseData getPurchaseData(@NonNull String productId) {
InAppPurchaseData data = SubscriptionUtils.getPurchaseData(ownedSubscriptions, productId);
if (data == null) {
for (OwnedPurchasesResult result : ownedInApps) {
data = InAppUtils.getPurchaseData(result, productId);
if (data != null) {
break;
}
}
}
return data;
}
private PurchaseInfo getPurchaseInfo(InAppPurchaseData purchase) {
return new PurchaseInfo(purchase.getProductId(), purchase.getOrderID(), purchase.getPurchaseToken());
}
private void fetchInAppPurchase(@NonNull InAppPurchase inAppPurchase, @NonNull ProductInfo productInfo, @Nullable InAppPurchaseData purchaseData) {
if (purchaseData != null) {
inAppPurchase.setPurchaseState(InAppPurchase.PurchaseState.PURCHASED);
inAppPurchase.setPurchaseTime(purchaseData.getPurchaseTime());
} else {
inAppPurchase.setPurchaseState(InAppPurchase.PurchaseState.NOT_PURCHASED);
}
inAppPurchase.setPrice(productInfo.getPrice());
inAppPurchase.setPriceCurrencyCode(productInfo.getCurrency());
if (productInfo.getMicrosPrice() > 0) {
inAppPurchase.setPriceValue(productInfo.getMicrosPrice() / 1000000d);
}
String subscriptionPeriod = productInfo.getSubPeriod();
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 = productInfo.getSubSpecialPrice();
String introductoryPricePeriod = productInfo.getSubPeriod();
int introductoryPriceCycles = productInfo.getSubSpecialPeriodCycles();
long introductoryPriceAmountMicros = productInfo.getSubSpecialPriceMicros();
if (!Algorithms.isEmpty(introductoryPrice)) {
InAppSubscription s = (InAppSubscription) inAppPurchase;
try {
s.setIntroductoryInfo(new InAppSubscriptionIntroductoryInfo(s, introductoryPrice,
introductoryPriceAmountMicros, introductoryPricePeriod, String.valueOf(introductoryPriceCycles)));
} catch (ParseException e) {
LOG.error(e);
}
}
}
}
protected InAppCommand getPurchaseLiveUpdatesCommand(final WeakReference<Activity> activity, final String sku, final String payload) {
return new InAppCommand() {
@Override
public void run(InAppPurchaseHelper helper) {
try {
Activity a = activity.get();
ProductInfo productInfo = getProductInfo(sku);
if (AndroidUtils.isActivityNotDestroyed(a) && productInfo != null) {
IapRequestHelper.createPurchaseIntent(getIapClient(), sku,
IapClient.PriceType.IN_APP_SUBSCRIPTION, payload, new IapApiCallback<PurchaseIntentResult>() {
@Override
public void onSuccess(PurchaseIntentResult result) {
if (result == null) {
logError("GetBuyIntentResult is null");
} else {
Activity a = activity.get();
if (AndroidUtils.isActivityNotDestroyed(a)) {
IapRequestHelper.startResolutionForResult(a, result.getStatus(), Constants.REQ_CODE_BUY_SUB);
} else {
logError("startResolutionForResult on destroyed activity");
}
}
commandDone();
}
@Override
public void onFail(Exception e) {
int errorCode = ExceptionHandle.handle(activity.get(), e);
if (ExceptionHandle.SOLVED != errorCode) {
logError("createPurchaseIntent, returnCode: " + errorCode);
if (OrderStatusCode.ORDER_PRODUCT_OWNED == errorCode) {
logError("already own this product");
} else {
logError("unknown error");
}
}
commandDone();
}
});
} else {
stop(true);
}
} catch (Exception e) {
logError("launchPurchaseFlow Error", e);
stop(true);
}
}
};
}
@Override
protected InAppCommand getRequestInventoryCommand() {
return new InAppCommand() {
@Override
protected void commandDone() {
super.commandDone();
inventoryRequested = false;
}
@Override
public void run(InAppPurchaseHelper helper) {
logDebug("Setup successful. Querying inventory.");
try {
productInfos = new ArrayList<>();
obtainOwnedSubscriptions();
} catch (Exception e) {
logError("queryInventoryAsync Error", e);
notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY);
stop(true);
commandDone();
}
}
private void obtainOwnedSubscriptions() {
if (uiActivity != null) {
IapRequestHelper.obtainOwnedPurchases(getIapClient(), IapClient.PriceType.IN_APP_SUBSCRIPTION,
null, new IapApiCallback<OwnedPurchasesResult>() {
@Override
public void onSuccess(OwnedPurchasesResult result) {
ownedSubscriptions = result;
obtainOwnedInApps(null);
}
@Override
public void onFail(Exception e) {
logError("obtainOwnedSubscriptions exception", e);
ExceptionHandle.handle((Activity) uiActivity, e);
commandDone();
}
});
} else {
commandDone();
}
}
private void obtainOwnedInApps(final String continuationToken) {
if (uiActivity != null) {
// Query users' purchased non-consumable products.
IapRequestHelper.obtainOwnedPurchases(getIapClient(), IapClient.PriceType.IN_APP_NONCONSUMABLE,
continuationToken, new IapApiCallback<OwnedPurchasesResult>() {
@Override
public void onSuccess(OwnedPurchasesResult result) {
ownedInApps.add(result);
if (result != null && !TextUtils.isEmpty(result.getContinuationToken())) {
obtainOwnedInApps(result.getContinuationToken());
} else {
obtainSubscriptionsInfo();
}
}
@Override
public void onFail(Exception e) {
logError("obtainOwnedInApps exception", e);
ExceptionHandle.handle((Activity) uiActivity, e);
commandDone();
}
});
} else {
commandDone();
}
}
private void obtainSubscriptionsInfo() {
if (uiActivity != null) {
Set<String> productIds = new HashSet<>();
List<InAppSubscription> subscriptions = purchases.getLiveUpdates().getAllSubscriptions();
for (InAppSubscription s : subscriptions) {
productIds.add(s.getSku());
}
productIds.addAll(ownedSubscriptions.getItemList());
IapRequestHelper.obtainProductInfo(getIapClient(), new ArrayList<>(productIds),
IapClient.PriceType.IN_APP_SUBSCRIPTION, new IapApiCallback<ProductInfoResult>() {
@Override
public void onSuccess(final ProductInfoResult result) {
if (result != null && result.getProductInfoList() != null) {
productInfos.addAll(result.getProductInfoList());
}
obtainInAppsInfo();
}
@Override
public void onFail(Exception e) {
int errorCode = ExceptionHandle.handle((Activity) uiActivity, e);
if (ExceptionHandle.SOLVED != errorCode) {
LOG.error("Unknown error");
}
commandDone();
}
});
} else {
commandDone();
}
}
private void obtainInAppsInfo() {
if (uiActivity != null) {
Set<String> productIds = new HashSet<>();
for (InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) {
productIds.add(purchase.getSku());
}
for (OwnedPurchasesResult result : ownedInApps) {
productIds.addAll(result.getItemList());
}
IapRequestHelper.obtainProductInfo(getIapClient(), new ArrayList<>(productIds),
IapClient.PriceType.IN_APP_NONCONSUMABLE, new IapApiCallback<ProductInfoResult>() {
@Override
public void onSuccess(ProductInfoResult result) {
if (result != null && result.getProductInfoList() != null) {
productInfos.addAll(result.getProductInfoList());
}
processInventory();
}
@Override
public void onFail(Exception e) {
int errorCode = ExceptionHandle.handle((Activity) uiActivity, e);
if (ExceptionHandle.SOLVED != errorCode) {
LOG.error("Unknown error");
}
commandDone();
}
});
} else {
commandDone();
}
}
private void processInventory() {
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!
*/
List<String> allOwnedSubscriptionSkus = ownedSubscriptions.getItemList();
for (InAppSubscription s : getLiveUpdates().getAllSubscriptions()) {
if (hasDetails(s.getSku())) {
InAppPurchaseData purchaseData = getPurchaseData(s.getSku());
ProductInfo liveUpdatesInfo = getProductInfo(s.getSku());
if (liveUpdatesInfo != null) {
fetchInAppPurchase(s, liveUpdatesInfo, purchaseData);
}
allOwnedSubscriptionSkus.remove(s.getSku());
}
}
for (String sku : allOwnedSubscriptionSkus) {
InAppPurchaseData purchaseData = getPurchaseData(sku);
ProductInfo liveUpdatesInfo = getProductInfo(sku);
if (liveUpdatesInfo != null) {
InAppSubscription s = getLiveUpdates().upgradeSubscription(sku);
if (s == null) {
s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesInfo);
}
fetchInAppPurchase(s, liveUpdatesInfo, purchaseData);
}
}
InAppPurchase fullVersion = getFullVersion();
if (hasDetails(fullVersion.getSku())) {
InAppPurchaseData purchaseData = getPurchaseData(fullVersion.getSku());
ProductInfo fullPriceDetails = getProductInfo(fullVersion.getSku());
if (fullPriceDetails != null) {
fetchInAppPurchase(fullVersion, fullPriceDetails, purchaseData);
}
}
InAppPurchase depthContours = getDepthContours();
if (hasDetails(depthContours.getSku())) {
InAppPurchaseData purchaseData = getPurchaseData(depthContours.getSku());
ProductInfo depthContoursDetails = getProductInfo(depthContours.getSku());
if (depthContoursDetails != null) {
fetchInAppPurchase(depthContours, depthContoursDetails, purchaseData);
}
}
InAppPurchase contourLines = getContourLines();
if (hasDetails(contourLines.getSku())) {
InAppPurchaseData purchaseData = getPurchaseData(contourLines.getSku());
ProductInfo contourLinesDetails = getProductInfo(contourLines.getSku());
if (contourLinesDetails != null) {
fetchInAppPurchase(contourLines, contourLinesDetails, purchaseData);
}
}
if (getPurchaseData(fullVersion.getSku()) != null) {
ctx.getSettings().FULL_VERSION_PURCHASED.set(true);
}
if (getPurchaseData(depthContours.getSku()) != null) {
ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true);
}
if (getPurchaseData(contourLines.getSku()) != null) {
ctx.getSettings().CONTOUR_LINES_PURCHASED.set(true);
}
// Do we have the live updates?
boolean subscribedToLiveUpdates = false;
List<InAppPurchaseData> liveUpdatesPurchases = new ArrayList<>();
for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) {
InAppPurchaseData purchaseData = getPurchaseData(p.getSku());
if (purchaseData != null) {
liveUpdatesPurchases.add(purchaseData);
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<InAppPurchaseData> tokensToSend = new ArrayList<>();
if (liveUpdatesPurchases.size() > 0) {
List<String> tokensSent = Arrays.asList(settings.BILLING_PURCHASE_TOKENS_SENT.get().split(";"));
for (InAppPurchaseData 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.getProductId())) {
tokensToSend.add(purchase);
}
}
}
List<PurchaseInfo> purchaseInfoList = new ArrayList<>();
for (InAppPurchaseData purchase : tokensToSend) {
purchaseInfoList.add(getPurchaseInfo(purchase));
}
onSkuDetailsResponseDone(purchaseInfoList);
}
};
}
private IapClient getIapClient() {
return Iap.getIapClient((Activity) uiActivity);
}
// Call when a purchase is finished
private void onPurchaseFinished(InAppPurchaseData purchase) {
logDebug("Purchase finished: " + purchase.getProductId());
onPurchaseDone(getPurchaseInfo(purchase));
}
@Override
protected boolean isBillingManagerExists() {
return false;
}
@Override
protected void destroyBillingManager() {
// non implemented
}
@Override
public boolean onActivityResult(@NonNull Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == Constants.REQ_CODE_BUY_SUB) {
boolean succeed = false;
if (resultCode == Activity.RESULT_OK) {
PurchaseResultInfo result = SubscriptionUtils.getPurchaseResult(activity, data);
if (result != null) {
switch (result.getReturnCode()) {
case OrderStatusCode.ORDER_STATE_CANCEL:
logDebug("Purchase cancelled");
break;
case OrderStatusCode.ORDER_STATE_FAILED:
inventoryRequestPending = true;
logDebug("Purchase failed");
break;
case OrderStatusCode.ORDER_PRODUCT_OWNED:
inventoryRequestPending = true;
logDebug("Product already owned");
break;
case OrderStatusCode.ORDER_STATE_SUCCESS:
inventoryRequestPending = true;
InAppPurchaseData purchaseData = SubscriptionUtils.getInAppPurchaseData(null,
result.getInAppPurchaseData(), result.getInAppDataSignature());
if (purchaseData != null) {
onPurchaseFinished(purchaseData);
succeed = true;
} else {
logDebug("Purchase failed");
}
break;
default:
break;
}
} else {
logDebug("Purchase failed");
}
} else {
logDebug("Purchase cancelled");
}
if (!succeed) {
stop(true);
}
return true;
} else if (requestCode == Constants.REQ_CODE_BUY_INAPP) {
boolean succeed = false;
if (data == null) {
logDebug("data is null");
} else {
PurchaseResultInfo buyResultInfo = Iap.getIapClient(activity).parsePurchaseResultInfoFromIntent(data);
switch (buyResultInfo.getReturnCode()) {
case OrderStatusCode.ORDER_STATE_CANCEL:
logDebug("Order has been canceled");
break;
case OrderStatusCode.ORDER_STATE_FAILED:
inventoryRequestPending = true;
logDebug("Order has been failed");
break;
case OrderStatusCode.ORDER_PRODUCT_OWNED:
inventoryRequestPending = true;
logDebug("Product already owned");
break;
case OrderStatusCode.ORDER_STATE_SUCCESS:
InAppPurchaseData purchaseData = InAppUtils.getInAppPurchaseData(null,
buyResultInfo.getInAppPurchaseData(), buyResultInfo.getInAppDataSignature());
if (purchaseData != null) {
onPurchaseFinished(purchaseData);
succeed = true;
} else {
logDebug("Purchase failed");
}
break;
default:
break;
}
}
if (!succeed) {
stop(true);
}
return true;
}
return false;
}
}

View file

@ -0,0 +1,196 @@
package net.osmand.plus.inapp;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.huawei.hms.iap.entity.ProductInfo;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.R;
public class InAppPurchasesImpl extends InAppPurchases {
private static final InAppPurchase FULL_VERSION = new InAppPurchaseFullVersion();
private static final InAppPurchaseDepthContoursFree DEPTH_CONTOURS_FREE = new InAppPurchaseDepthContoursFree();
private static final InAppPurchaseContourLinesFree CONTOUR_LINES_FREE = new InAppPurchaseContourLinesFree();
private static final InAppSubscription[] LIVE_UPDATES_FREE = new InAppSubscription[]{
new InAppPurchaseLiveUpdatesMonthlyFree(),
new InAppPurchaseLiveUpdates3MonthsFree(),
new InAppPurchaseLiveUpdatesAnnualFree()
};
public InAppPurchasesImpl(OsmandApplication ctx) {
super(ctx);
fullVersion = FULL_VERSION;
depthContours = DEPTH_CONTOURS_FREE;
contourLines = CONTOUR_LINES_FREE;
inAppPurchases = new InAppPurchase[] { fullVersion, depthContours, contourLines };
liveUpdates = new LiveUpdatesInAppPurchasesFree();
for (InAppSubscription s : liveUpdates.getAllSubscriptions()) {
if (s instanceof InAppPurchaseLiveUpdatesMonthly) {
if (s.isDiscounted()) {
discountedMonthlyLiveUpdates = s;
} else {
monthlyLiveUpdates = s;
}
}
}
}
@Override
public boolean isFullVersion(String sku) {
return FULL_VERSION.getSku().equals(sku);
}
@Override
public boolean isDepthContours(String sku) {
return DEPTH_CONTOURS_FREE.getSku().equals(sku);
}
@Override
public boolean isContourLines(String sku) {
return CONTOUR_LINES_FREE.getSku().equals(sku);
}
@Override
public boolean isLiveUpdates(String sku) {
for (InAppPurchase p : LIVE_UPDATES_FREE) {
if (p.getSku().equals(sku)) {
return true;
}
}
return false;
}
private static class InAppPurchaseFullVersion extends InAppPurchase {
private static final String SKU_FULL_VERSION_PRICE = "net.osmand.huawei.full";
InAppPurchaseFullVersion() {
super(SKU_FULL_VERSION_PRICE);
}
@Override
public String getDefaultPrice(Context ctx) {
return ctx.getString(R.string.full_version_price);
}
}
private static class InAppPurchaseDepthContoursFree extends InAppPurchaseDepthContours {
private static final String SKU_DEPTH_CONTOURS_FREE = "net.osmand.huawei.seadepth";
InAppPurchaseDepthContoursFree() {
super(SKU_DEPTH_CONTOURS_FREE);
}
}
private static class InAppPurchaseContourLinesFree extends InAppPurchaseContourLines {
private static final String SKU_CONTOUR_LINES_FREE = "net.osmand.huawei.contourlines";
InAppPurchaseContourLinesFree() {
super(SKU_CONTOUR_LINES_FREE);
}
}
private static class InAppPurchaseLiveUpdatesMonthlyFree extends InAppPurchaseLiveUpdatesMonthly {
private static final String SKU_LIVE_UPDATES_MONTHLY_HW_FREE = "net.osmand.huawei.monthly";
InAppPurchaseLiveUpdatesMonthlyFree() {
super(SKU_LIVE_UPDATES_MONTHLY_HW_FREE, 1);
}
private InAppPurchaseLiveUpdatesMonthlyFree(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFree(sku) : null;
}
}
private static class InAppPurchaseLiveUpdates3MonthsFree extends InAppPurchaseLiveUpdates3Months {
private static final String SKU_LIVE_UPDATES_3_MONTHS_HW_FREE = "net.osmand.huawei.3months";
InAppPurchaseLiveUpdates3MonthsFree() {
super(SKU_LIVE_UPDATES_3_MONTHS_HW_FREE, 1);
}
private InAppPurchaseLiveUpdates3MonthsFree(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFree(sku) : null;
}
}
private static class InAppPurchaseLiveUpdatesAnnualFree extends InAppPurchaseLiveUpdatesAnnual {
private static final String SKU_LIVE_UPDATES_ANNUAL_HW_FREE = "net.osmand.huawei.annual";
InAppPurchaseLiveUpdatesAnnualFree() {
super(SKU_LIVE_UPDATES_ANNUAL_HW_FREE, 1);
}
private InAppPurchaseLiveUpdatesAnnualFree(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFree(sku) : null;
}
}
public static class InAppPurchaseLiveUpdatesOldSubscription extends InAppSubscription {
private ProductInfo info;
InAppPurchaseLiveUpdatesOldSubscription(@NonNull ProductInfo info) {
super(info.getProductId(), true);
this.info = info;
}
@Override
public String getDefaultPrice(Context ctx) {
return "";
}
@Override
public CharSequence getTitle(Context ctx) {
return info.getProductName();
}
@Override
public CharSequence getDescription(@NonNull Context ctx) {
return info.getProductDesc();
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return null;
}
}
private static class LiveUpdatesInAppPurchasesFree extends InAppSubscriptionList {
public LiveUpdatesInAppPurchasesFree() {
super(LIVE_UPDATES_FREE);
}
}
}

View file

@ -0,0 +1,49 @@
package net.osmand.plus.inapp;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.huawei.hms.iap.entity.InAppPurchaseData;
import com.huawei.hms.iap.entity.OwnedPurchasesResult;
import org.json.JSONException;
public class InAppUtils {
private static final String TAG = "InAppUtils";
@Nullable
public static InAppPurchaseData getPurchaseData(OwnedPurchasesResult result, String productId) {
if (result == null || result.getInAppPurchaseDataList() == null) {
Log.i(TAG, "result is null");
return null;
}
int index = result.getItemList().indexOf(productId);
if (index != -1) {
String data = result.getInAppPurchaseDataList().get(index);
String signature = result.getInAppSignature().get(index);
return getInAppPurchaseData(productId, data, signature);
}
return null;
}
@Nullable
public static InAppPurchaseData getInAppPurchaseData(@Nullable String productId, @NonNull String data, @NonNull String signature) {
if (CipherUtil.doCheck(data, signature, CipherUtil.getPublicKey())) {
try {
InAppPurchaseData purchaseData = new InAppPurchaseData(data);
if (purchaseData.getPurchaseState() == InAppPurchaseData.PurchaseState.PURCHASED) {
if (productId == null || productId.equals(purchaseData.getProductId())) {
return purchaseData;
}
}
} catch (JSONException e) {
Log.e(TAG, "delivery: " + e.getMessage());
}
} else {
Log.e(TAG, "delivery: verify signature error");
}
return null;
}
}

View file

@ -0,0 +1,138 @@
/**
* Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp;
import android.app.Activity;
import android.content.Intent;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.huawei.hms.iap.Iap;
import com.huawei.hms.iap.entity.InAppPurchaseData;
import com.huawei.hms.iap.entity.OrderStatusCode;
import com.huawei.hms.iap.entity.OwnedPurchasesResult;
import com.huawei.hms.iap.entity.PurchaseResultInfo;
import org.json.JSONException;
import java.util.List;
/**
* Util for Subscription function.
*
* @since 2019/12/9
*/
public class SubscriptionUtils {
private static final String TAG = "SubscriptionUtils";
/**
* Decide whether to offer subscription service
*
* @param result the OwnedPurchasesResult from IapClient.obtainOwnedPurchases
* @param productId subscription product id
* @return decision result
*/
@Nullable
public static InAppPurchaseData getPurchaseData(OwnedPurchasesResult result, String productId) {
if (null == result) {
Log.e(TAG, "OwnedPurchasesResult is null");
return null;
}
List<String> dataList = result.getInAppPurchaseDataList();
List<String> signatureList = result.getInAppSignature();
for (int i = 0; i < dataList.size(); i++) {
String data = dataList.get(i);
String signature = signatureList.get(i);
InAppPurchaseData purchaseData = getInAppPurchaseData(productId, data, signature);
if (purchaseData != null) {
return purchaseData;
}
}
return null;
}
@Nullable
public static InAppPurchaseData getInAppPurchaseData(@Nullable String productId, @NonNull String data, @NonNull String signature) {
try {
InAppPurchaseData purchaseData = new InAppPurchaseData(data);
if (productId == null || productId.equals(purchaseData.getProductId())) {
boolean credible = CipherUtil.doCheck(data, signature, CipherUtil.getPublicKey());
if (credible) {
return purchaseData.isSubValid() ? purchaseData : null;
} else {
Log.e(TAG, "check the data signature fail");
return null;
}
}
} catch (JSONException e) {
Log.e(TAG, "parse InAppPurchaseData JSONException", e);
return null;
}
return null;
}
/**
* Parse PurchaseResult data from intent
*
* @param activity Activity
* @param data the intent from onActivityResult
* @return PurchaseResultInfo
*/
public static PurchaseResultInfo getPurchaseResult(Activity activity, Intent data) {
PurchaseResultInfo purchaseResultInfo = Iap.getIapClient(activity).parsePurchaseResultInfoFromIntent(data);
if (null == purchaseResultInfo) {
Log.e(TAG, "PurchaseResultInfo is null");
} else {
int returnCode = purchaseResultInfo.getReturnCode();
String errMsg = purchaseResultInfo.getErrMsg();
switch (returnCode) {
case OrderStatusCode.ORDER_PRODUCT_OWNED:
Log.w(TAG, "you have owned this product");
break;
case OrderStatusCode.ORDER_STATE_SUCCESS:
boolean credible = CipherUtil.doCheck(purchaseResultInfo.getInAppPurchaseData(), purchaseResultInfo.getInAppDataSignature(), CipherUtil
.getPublicKey());
if (credible) {
try {
InAppPurchaseData inAppPurchaseData = new InAppPurchaseData(purchaseResultInfo.getInAppPurchaseData());
if (!inAppPurchaseData.isSubValid()) {
return getFailedPurchaseResultInfo();
}
} catch (JSONException e) {
Log.e(TAG, "parse InAppPurchaseData JSONException", e);
return getFailedPurchaseResultInfo();
}
} else {
Log.e(TAG, "check the data signature fail");
return getFailedPurchaseResultInfo();
}
default:
Log.e(TAG, "returnCode: " + returnCode + " , errMsg: " + errMsg);
break;
}
}
return purchaseResultInfo;
}
private static PurchaseResultInfo getFailedPurchaseResultInfo() {
PurchaseResultInfo info = new PurchaseResultInfo();
info.setReturnCode(OrderStatusCode.ORDER_STATE_FAILED);
return info;
}
}

View file

@ -38,7 +38,7 @@ import net.osmand.plus.download.ui.AbstractLoadLocalIndexTask;
import net.osmand.plus.helpers.AvoidSpecificRoads;
import net.osmand.plus.helpers.LockHelper;
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.mapmarkers.MapMarkersDbHelper;
import net.osmand.plus.monitoring.LiveMonitoringHelper;
@ -428,7 +428,7 @@ public class AppInitializer implements IProgress {
}
getLazyRoutingConfig();
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.transportRoutingHelper = startupInit(new TransportRoutingHelper(app), TransportRoutingHelper.class);
app.routingHelper = startupInit(new RoutingHelper(app), RoutingHelper.class);

View file

@ -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);
}
}

View file

@ -121,8 +121,8 @@ public class Version {
public static boolean isFreeVersion(OsmandApplication ctx){
return ctx.getPackageName().equals(FREE_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) {

View file

@ -67,7 +67,6 @@ import net.osmand.plus.AppInitializer;
import net.osmand.plus.AppInitializer.AppInitializeListener;
import net.osmand.plus.AppInitializer.InitEvents;
import net.osmand.plus.GpxSelectionHelper.GpxDisplayItem;
import net.osmand.plus.HuaweiDrmHelper;
import net.osmand.plus.MapMarkersHelper.MapMarker;
import net.osmand.plus.MapMarkersHelper.MapMarkerChangedListener;
import net.osmand.plus.OnDismissDialogFragmentListener;
@ -276,9 +275,6 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven
super.onCreate(savedInstanceState);
if (Version.isHuawei(getMyApplication())) {
HuaweiDrmHelper.check(this);
}
// Full screen is not used here
// getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.main);

View file

@ -899,7 +899,7 @@ public class MapActivityActions implements DialogProvider {
}
}).createItem());
if (Version.isGooglePlayEnabled(app) || Version.isDeveloperVersion(app)) {
if (Version.isGooglePlayEnabled(app) || Version.isHuawei(app) || Version.isDeveloperVersion(app)) {
optionsMenuHelper.addItem(new ItemBuilder().setTitleId(R.string.osm_live, mapActivity)
.setId(DRAWER_OSMAND_LIVE_ID)
.setIcon(R.drawable.ic_action_osm_live)

View file

@ -5,7 +5,6 @@ import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -14,12 +13,15 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import net.osmand.AndroidUtils;
import net.osmand.PlatformUtil;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.OsmandPlugin;
import net.osmand.plus.R;
import net.osmand.plus.Version;
import net.osmand.plus.download.DownloadActivity;
import net.osmand.plus.inapp.InAppPurchaseHelper;
import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseInitCallback;
import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseListener;
import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseTaskType;
import net.osmand.plus.liveupdates.OsmLiveRestartBottomSheetDialogFragment;
@ -27,6 +29,7 @@ import net.osmand.plus.srtmplugin.SRTMPlugin;
import org.apache.commons.logging.Log;
import java.lang.ref.WeakReference;
import java.util.List;
@SuppressLint("Registered")
@ -34,14 +37,7 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In
private static final Log LOG = PlatformUtil.getLog(OsmandInAppPurchaseActivity.class);
private InAppPurchaseHelper purchaseHelper;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (isInAppPurchaseAllowed() && isInAppPurchaseSupported()) {
purchaseHelper = getMyApplication().getInAppPurchaseHelper();
}
}
private boolean activityDestroyed;
@Override
protected void onResume() {
@ -53,19 +49,41 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In
protected void onDestroy() {
super.onDestroy();
deinitInAppPurchaseHelper();
activityDestroyed = true;
}
private void initInAppPurchaseHelper() {
deinitInAppPurchaseHelper();
if (purchaseHelper == null) {
OsmandApplication app = getMyApplication();
InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper();
if (app.getSettings().isInternetConnectionAvailable()
&& isInAppPurchaseAllowed()
&& isInAppPurchaseSupported(purchaseHelper)) {
this.purchaseHelper = purchaseHelper;
}
}
if (purchaseHelper != null) {
purchaseHelper.setUiActivity(this);
final WeakReference<OsmandInAppPurchaseActivity> activityRef = new WeakReference<>(this);
purchaseHelper.isInAppPurchaseSupported(this, new InAppPurchaseInitCallback() {
@Override
public void onSuccess() {
OsmandInAppPurchaseActivity activity = activityRef.get();
if (!activityDestroyed && AndroidUtils.isActivityNotDestroyed(activity)) {
purchaseHelper.setUiActivity(activity);
if (purchaseHelper.needRequestInventory()) {
purchaseHelper.requestInventory();
}
}
}
@Override
public void onFail() {
}
});
}
}
private void deinitInAppPurchaseHelper() {
if (purchaseHelper != null) {
purchaseHelper.resetUiActivity(this);
@ -80,7 +98,11 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In
InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper();
if (purchaseHelper != null) {
app.logEvent("in_app_purchase_redirect");
try {
purchaseHelper.purchaseFullVersion(activity);
} catch (UnsupportedOperationException e) {
LOG.error("purchaseFullVersion is not supported", e);
}
}
} else {
app.logEvent("paid_version_redirect");
@ -101,18 +123,27 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In
InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper();
if (purchaseHelper != null) {
app.logEvent("depth_contours_purchase_redirect");
try {
purchaseHelper.purchaseDepthContours(activity);
} catch (UnsupportedOperationException e) {
LOG.error("purchaseDepthContours is not supported", e);
}
}
}
}
public static void purchaseSrtmPlugin(@NonNull final Activity activity) {
OsmandPlugin plugin = OsmandPlugin.getPlugin(SRTMPlugin.class);
if(plugin == null || plugin.getInstallURL() == null) {
Toast.makeText(activity.getApplicationContext(),
activity.getString(R.string.activate_srtm_plugin), Toast.LENGTH_LONG).show();
} else {
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(plugin.getInstallURL())));
public static void purchaseContourLines(@NonNull final Activity activity) {
OsmandApplication app = (OsmandApplication) activity.getApplication();
if (app != null) {
InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper();
if (purchaseHelper != null) {
app.logEvent("contour_lines_purchase_redirect");
try {
purchaseHelper.purchaseContourLines(activity);
} catch (UnsupportedOperationException e) {
LOG.error("purchaseContourLines is not supported", e);
}
}
}
}
@ -129,8 +160,9 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In
return false;
}
public boolean isInAppPurchaseSupported() {
return Version.isGooglePlayEnabled(getMyApplication());
public boolean isInAppPurchaseSupported(InAppPurchaseHelper purchaseHelper) {
OsmandApplication app = getMyApplication();
return Version.isGooglePlayEnabled(app) || Version.isHuawei(app);
}
@Override
@ -178,6 +210,11 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In
}
onInAppPurchaseItemPurchased(sku);
fireInAppPurchaseItemPurchasedOnFragments(fragmentManager, sku, active);
if (purchaseHelper != null && purchaseHelper.getContourLines().getSku().equals(sku)) {
if (!(this instanceof MapActivity)) {
finish();
}
}
}
public void fireInAppPurchaseItemPurchasedOnFragments(@NonNull FragmentManager fragmentManager,
@ -222,6 +259,17 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
boolean handled = false;
if (purchaseHelper != null) {
handled = purchaseHelper.onActivityResult(this, requestCode, resultCode, data);
}
if (!handled) {
super.onActivityResult(requestCode, resultCode, data);
}
}
public void onInAppPurchaseError(InAppPurchaseTaskType taskType, String error) {
// not implemented
}

View file

@ -428,7 +428,7 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment
buttonCancelView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
manageSubscription(ctx, s.getSku());
purchaseHelper.manageSubscription(ctx, s.getSku());
}
});
div.setVisibility(View.VISIBLE);
@ -538,15 +538,6 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment
}
}
private void manageSubscription(@NonNull Context ctx, @Nullable String sku) {
String url = "https://play.google.com/store/account/subscriptions?package=" + ctx.getPackageName();
if (!Algorithms.isEmpty(sku)) {
url += "&sku=" + sku;
}
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
}
private ViewGroup buildPlanTypeCard(@NonNull Context ctx, ViewGroup container) {
if (getPlanTypeFeatures().length == 0) {
return null;

View file

@ -79,7 +79,7 @@ public class ChoosePlanHillshadeSrtmDialogFragment extends ChoosePlanDialogFragm
public void onClick(View v) {
Activity activity = getActivity();
if (activity != null) {
OsmandInAppPurchaseActivity.purchaseSrtmPlugin(activity);
OsmandInAppPurchaseActivity.purchaseContourLines(activity);
dismiss();
}
}

View file

@ -20,12 +20,14 @@ import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import net.osmand.AndroidNetworkUtils;
import net.osmand.PlatformUtil;
import net.osmand.osm.AbstractPoiType;
import net.osmand.osm.MapPoiTypes;
import net.osmand.osm.PoiCategory;
import net.osmand.osm.PoiType;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.OsmandPlugin;
import net.osmand.plus.activities.OsmandInAppPurchaseActivity;
import net.osmand.plus.settings.backend.OsmandSettings;
import net.osmand.plus.R;
import net.osmand.plus.Version;
@ -56,7 +58,7 @@ import java.util.Map;
public class DiscountHelper {
private static final String TAG = "DiscountHelper";
//private static final String DISCOUNT_JSON = "discount.json";
private static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(DiscountHelper.class);
private static long mLastCheckTime;
private static ControllerData mData;
@ -81,7 +83,7 @@ public class DiscountHelper {
public static void checkAndDisplay(final MapActivity mapActivity) {
OsmandApplication app = mapActivity.getMyApplication();
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;
}
if (mBannerVisible) {
@ -312,7 +314,11 @@ public class DiscountHelper {
if (purchaseHelper != null) {
if (url.contains(purchaseHelper.getFullVersion().getSku())) {
app.logEvent("in_app_purchase_redirect");
try {
purchaseHelper.purchaseFullVersion(mapActivity);
} catch (UnsupportedOperationException e) {
LOG.error("purchaseFullVersion is not supported", e);
}
} else {
for (InAppPurchase p : purchaseHelper.getLiveUpdates().getAllSubscriptions()) {
if (url.contains(p.getSku())) {

View file

@ -2,6 +2,8 @@ package net.osmand.plus.inapp;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
@ -9,33 +11,21 @@ import android.util.Log;
import androidx.annotation.NonNull;
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.OnRequestResultListener;
import net.osmand.AndroidNetworkUtils.OnRequestsResultListener;
import net.osmand.AndroidNetworkUtils.RequestResponse;
import net.osmand.PlatformUtil;
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.Version;
import net.osmand.plus.inapp.InAppPurchases.InAppPurchase;
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.InAppSubscriptionIntroductoryInfo;
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.CountryItem;
import net.osmand.plus.settings.backend.OsmandSettings;
import net.osmand.util.Algorithms;
import org.json.JSONArray;
@ -43,7 +33,6 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -53,54 +42,31 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
public class InAppPurchaseHelper {
public abstract class InAppPurchaseHelper {
// 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 boolean mDebugLog = false;
public static final long SUBSCRIPTION_HOLDING_TIME_MSEC = 1000 * 60 * 60 * 24 * 3; // 3 days
private InAppPurchases purchases;
private long lastValidationCheckTime;
private boolean inventoryRequested;
protected InAppPurchases purchases;
protected long lastValidationCheckTime;
protected boolean inventoryRequested;
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
private BillingManager billingManager;
private List<SkuDetails> skuDetailsList;
protected boolean isDeveloperVersion;
protected String token = "";
protected InAppPurchaseTaskType activeTask;
protected boolean processingTask = false;
protected boolean inventoryRequestPending = false;
private boolean isDeveloperVersion;
private String token = "";
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";
protected OsmandApplication ctx;
protected InAppPurchaseListener uiActivity = null;
public interface InAppPurchaseListener {
void onError(InAppPurchaseTaskType taskType, String error);
void onGetItems();
@ -112,16 +78,62 @@ public class InAppPurchaseHelper {
void dismissProgress(InAppPurchaseTaskType taskType);
}
public interface InAppPurchaseInitCallback {
void onSuccess();
void onFail();
}
public enum InAppPurchaseTaskType {
REQUEST_INVENTORY,
PURCHASE_FULL_VERSION,
PURCHASE_LIVE_UPDATES,
PURCHASE_DEPTH_CONTOURS
PURCHASE_DEPTH_CONTOURS,
PURCHASE_CONTOUR_LINES
}
public interface InAppRunnable {
public abstract class InAppCommand {
InAppCommandResultHandler resultHandler;
// return true if done and false if async task started
boolean run(InAppPurchaseHelper helper);
abstract void run(InAppPurchaseHelper helper);
protected void commandDone() {
InAppCommandResultHandler resultHandler = this.resultHandler;
if (resultHandler != null) {
resultHandler.onCommandDone(this);
}
}
}
public interface InAppCommandResultHandler {
void onCommandDone(@NonNull InAppCommand command);
}
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() {
@ -144,6 +156,10 @@ public class InAppPurchaseHelper {
return Version.isDeveloperBuild(ctx) || ctx.getSettings().DEPTH_CONTOURS_PURCHASED.get();
}
public static boolean isContourLinesPurchased(@NonNull OsmandApplication ctx) {
return Version.isDeveloperBuild(ctx) || ctx.getSettings().CONTOUR_LINES_PURCHASED.get();
}
public InAppPurchases getInAppPurchases() {
return purchases;
}
@ -176,9 +192,10 @@ public class InAppPurchaseHelper {
public InAppPurchaseHelper(OsmandApplication ctx) {
this.ctx = ctx;
isDeveloperVersion = Version.isDeveloperVersion(ctx);
purchases = new InAppPurchases(ctx);
}
public abstract void isInAppPurchaseSupported(@NonNull final Activity activity, @Nullable final InAppPurchaseInitCallback callback);
public boolean hasInventory() {
return lastValidationCheckTime != 0;
}
@ -194,12 +211,8 @@ public class InAppPurchaseHelper {
return false;
}
private BillingManager getBillingManager() {
return billingManager;
}
private void exec(final @NonNull InAppPurchaseTaskType taskType, final @NonNull InAppRunnable runnable) {
if (isDeveloperVersion || !Version.isGooglePlayEnabled(ctx)) {
protected void exec(final @NonNull InAppPurchaseTaskType taskType, final @NonNull InAppCommand command) {
if (isDeveloperVersion || (!Version.isGooglePlayEnabled(ctx) && !Version.isHuawei(ctx))) {
notifyDismissProgress(taskType);
stop(true);
return;
@ -222,117 +235,21 @@ public class InAppPurchaseHelper {
try {
processingTask = true;
activeTask = taskType;
billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingUpdatesListener() {
command.resultHandler = new InAppCommandResultHandler() {
@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;
public void onCommandDone(@NonNull InAppCommand command) {
processingTask = false;
}
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);
}
});
};
execImpl(taskType, command);
} catch (Exception e) {
logError("exec Error", e);
stop(true);
}
}
protected abstract void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppCommand command);
public boolean needRequestInventory() {
return !inventoryRequested && ((isSubscribedToLiveUpdates(ctx) && Algorithms.isEmpty(ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.get()))
|| System.currentTimeMillis() - lastValidationCheckTime > PURCHASE_VALIDATION_PERIOD_MSEC);
@ -343,322 +260,20 @@ public class InAppPurchaseHelper {
new RequestInventoryTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
}
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 abstract void purchaseFullVersion(@NonNull final Activity activity) throws UnsupportedOperationException;
public void purchaseLiveUpdates(Activity activity, String sku, String email, String userName,
public void purchaseLiveUpdates(@NonNull Activity activity, String sku, String email, String userName,
String countryDownloadName, boolean hideUserName) {
notifyShowProgress(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES);
new LiveUpdatesPurchaseTask(activity, sku, email, userName, countryDownloadName, hideUserName)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
}
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;
}
});
}
public abstract void purchaseDepthContours(@NonNull final Activity activity) throws 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;
}
public abstract void purchaseContourLines(@NonNull final Activity activity) throws UnsupportedOperationException;
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);
}
}
}
}
public abstract void manageSubscription(@NonNull Context ctx, @Nullable String sku);
@SuppressLint("StaticFieldLeak")
private class LiveUpdatesPurchaseTask extends AsyncTask<Void, Void, String> {
@ -746,31 +361,7 @@ public class InAppPurchaseHelper {
if (!Algorithms.isEmpty(userId) && !Algorithms.isEmpty(token)) {
logDebug("Launching purchase flow for live updates subscription for userId=" + userId);
final String payload = userId + " " + token;
exec(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, new InAppRunnable() {
@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;
}
});
exec(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, getPurchaseLiveUpdatesCommand(activity, sku, payload));
} else {
notifyError(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, "Empty userId");
stop(true);
@ -778,6 +369,9 @@ public class InAppPurchaseHelper {
}
}
protected abstract InAppCommand getPurchaseLiveUpdatesCommand(final WeakReference<Activity> activity,
final String sku, final String payload) throws UnsupportedOperationException;
@SuppressLint("StaticFieldLeak")
private class RequestInventoryTask extends AsyncTask<Void, Void, String> {
@ -808,6 +402,7 @@ public class InAppPurchaseHelper {
try {
JSONObject obj = new JSONObject(response);
JSONArray names = obj.names();
if (names != null) {
for (int i = 0; i < names.length(); i++) {
String skuType = names.getString(i);
JSONObject subObj = obj.getJSONObject(skuType);
@ -816,30 +411,32 @@ public class InAppPurchaseHelper {
getLiveUpdates().upgradeSubscription(sku);
}
}
}
} catch (JSONException e) {
logError("Json parsing error", e);
}
}
exec(InAppPurchaseTaskType.REQUEST_INVENTORY, new InAppRunnable() {
exec(InAppPurchaseTaskType.REQUEST_INVENTORY, getRequestInventoryCommand());
}
}
protected abstract InAppCommand getRequestInventoryCommand() throws UnsupportedOperationException;
protected void onSkuDetailsResponseDone(List<PurchaseInfo> purchaseInfoList) {
final AndroidNetworkUtils.OnRequestResultListener listener = new AndroidNetworkUtils.OnRequestResultListener() {
@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);
public void onResult(String result) {
notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY);
notifyGetItems();
stop(true);
logDebug("Initial inapp query finished");
}
return true;
}
});
};
if (purchaseInfoList.size() > 0) {
sendTokens(purchaseInfoList, listener);
} else {
listener.onResult("OK");
}
}
@ -852,25 +449,16 @@ public class InAppPurchaseHelper {
parameters.put("aid", ctx.getUserAndroidId());
}
// 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;
}
protected void onPurchaseDone(PurchaseInfo info) {
logDebug("Purchase successful.");
InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(purchase.getSku());
InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(info.getSku());
if (liveUpdatesPurchase != null) {
// bought live updates
logDebug("Live updates subscription purchased.");
final String sku = liveUpdatesPurchase.getSku();
liveUpdatesPurchase.setPurchaseState(PurchaseState.PURCHASED);
sendTokens(Collections.singletonList(purchase), new OnRequestResultListener() {
sendTokens(Collections.singletonList(info), new OnRequestResultListener() {
@Override
public void onResult(String result) {
boolean active = ctx.getSettings().LIVE_UPDATES_PURCHASED.get();
@ -887,7 +475,7 @@ public class InAppPurchaseHelper {
}
});
} else if (purchase.getSku().equals(getFullVersion().getSku())) {
} else if (info.getSku().equals(getFullVersion().getSku())) {
// bought full version
getFullVersion().setPurchaseState(PurchaseState.PURCHASED);
logDebug("Full version purchased.");
@ -898,7 +486,7 @@ public class InAppPurchaseHelper {
notifyItemPurchased(getFullVersion().getSku(), false);
stop(true);
} else if (purchase.getSku().equals(getDepthContours().getSku())) {
} else if (info.getSku().equals(getDepthContours().getSku())) {
// bought sea depth contours
getDepthContours().setPurchaseState(PurchaseState.PURCHASED);
logDebug("Sea depth contours purchased.");
@ -910,6 +498,17 @@ public class InAppPurchaseHelper {
notifyItemPurchased(getDepthContours().getSku(), false);
stop(true);
} else if (info.getSku().equals(getContourLines().getSku())) {
// bought contour lines
getContourLines().setPurchaseState(PurchaseState.PURCHASED);
logDebug("Contours lines purchased.");
showToast(ctx.getString(R.string.contour_lines_thanks));
ctx.getSettings().CONTOUR_LINES_PURCHASED.set(true);
notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_CONTOUR_LINES);
notifyItemPurchased(getContourLines().getSku(), false);
stop(true);
} else {
notifyDismissProgress(activeTask);
stop(true);
@ -921,17 +520,19 @@ public class InAppPurchaseHelper {
stop(false);
}
private void stop(boolean taskDone) {
protected abstract boolean isBillingManagerExists();
protected abstract void destroyBillingManager();
protected void stop(boolean taskDone) {
logDebug("Destroying helper.");
BillingManager billingManager = getBillingManager();
if (billingManager != null) {
if (isBillingManagerExists()) {
if (taskDone) {
processingTask = false;
}
if (!processingTask) {
activeTask = null;
billingManager.destroy();
this.billingManager = null;
destroyBillingManager();
}
} else {
processingTask = false;
@ -943,7 +544,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 token = ctx.getSettings().BILLING_USER_TOKEN.get();
final String email = ctx.getSettings().BILLING_USER_EMAIL.get();
@ -951,12 +552,12 @@ public class InAppPurchaseHelper {
String url = "https://osmand.net/subscription/purchased";
String userOperation = "Sending purchase info...";
final List<AndroidNetworkUtils.Request> requests = new ArrayList<>();
for (Purchase purchase : purchases) {
for (PurchaseInfo info : purchaseInfoList) {
Map<String, String> parameters = new HashMap<>();
parameters.put("userid", userId);
parameters.put("sku", purchase.getSku());
parameters.put("orderId", purchase.getOrderId());
parameters.put("purchaseToken", purchase.getPurchaseToken());
parameters.put("sku", info.getSku());
parameters.put("orderId", info.getOrderId());
parameters.put("purchaseToken", info.getPurchaseToken());
parameters.put("email", email);
parameters.put("token", token);
addUserInfo(parameters);
@ -967,9 +568,9 @@ public class InAppPurchaseHelper {
public void onResult(@NonNull List<RequestResponse> results) {
for (RequestResponse rr : results) {
String sku = rr.getRequest().getParameters().get("sku");
Purchase purchase = getPurchase(sku);
if (purchase != null) {
updateSentTokens(purchase);
PurchaseInfo info = getPurchaseInfo(sku);
if (info != null) {
updateSentTokens(info);
String result = rr.getResponse();
if (result != null) {
try {
@ -979,13 +580,13 @@ public class InAppPurchaseHelper {
} else {
complain("SendToken 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) {
logError("SendToken", e);
complain("SendToken Error: "
+ (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 +596,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();
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));
}
@ -1032,10 +633,10 @@ public class InAppPurchaseHelper {
}
@Nullable
private Purchase getPurchase(String sku) {
for (Purchase purchase : purchases) {
if (purchase.getSku().equals(sku)) {
return purchase;
private PurchaseInfo getPurchaseInfo(String sku) {
for (PurchaseInfo info : purchaseInfoList) {
if (info.getSku().equals(sku)) {
return info;
}
}
return null;
@ -1049,31 +650,35 @@ public class InAppPurchaseHelper {
}
}
private void notifyError(InAppPurchaseTaskType taskType, String message) {
public boolean onActivityResult(@NonNull Activity activity, int requestCode, int resultCode, Intent data) {
return false;
}
protected void notifyError(InAppPurchaseTaskType taskType, String message) {
if (uiActivity != null) {
uiActivity.onError(taskType, message);
}
}
private void notifyGetItems() {
protected void notifyGetItems() {
if (uiActivity != null) {
uiActivity.onGetItems();
}
}
private void notifyItemPurchased(String sku, boolean active) {
protected void notifyItemPurchased(String sku, boolean active) {
if (uiActivity != null) {
uiActivity.onItemPurchased(sku, active);
}
}
private void notifyShowProgress(InAppPurchaseTaskType taskType) {
protected void notifyShowProgress(InAppPurchaseTaskType taskType) {
if (uiActivity != null) {
uiActivity.showProgress(taskType);
}
}
private void notifyDismissProgress(InAppPurchaseTaskType taskType) {
protected void notifyDismissProgress(InAppPurchaseTaskType taskType) {
if (uiActivity != null) {
uiActivity.dismissProgress(taskType);
}
@ -1090,26 +695,26 @@ public class InAppPurchaseHelper {
}
}
private void complain(String message) {
protected void complain(String message) {
logError("**** InAppPurchaseHelper Error: " + message);
showToast(message);
}
private void showToast(final String message) {
protected void showToast(final String message) {
ctx.showToastMessage(message);
}
private void logDebug(String msg) {
protected void logDebug(String msg) {
if (mDebugLog) {
Log.d(TAG, msg);
}
}
private void logError(String msg) {
protected void logError(String 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);
}

View file

@ -11,14 +11,11 @@ import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.billingclient.api.SkuDetails;
import net.osmand.AndroidUtils;
import net.osmand.Period;
import net.osmand.Period.PeriodUnit;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.R;
import net.osmand.plus.Version;
import net.osmand.plus.helpers.FontCache;
import net.osmand.plus.widgets.style.CustomTypefaceSpan;
import net.osmand.util.Algorithms;
@ -33,64 +30,17 @@ import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class InAppPurchases {
public abstract class InAppPurchases {
private static final InAppPurchase FULL_VERSION = new InAppPurchaseFullVersion();
private static final InAppPurchaseDepthContoursFull DEPTH_CONTOURS_FULL = new InAppPurchaseDepthContoursFull();
private static final InAppPurchaseDepthContoursFree DEPTH_CONTOURS_FREE = new InAppPurchaseDepthContoursFree();
private static final InAppPurchaseContourLinesFull CONTOUR_LINES_FULL = new InAppPurchaseContourLinesFull();
private static final InAppPurchaseContourLinesFree CONTOUR_LINES_FREE = new InAppPurchaseContourLinesFree();
protected InAppPurchase fullVersion;
protected InAppPurchase depthContours;
protected InAppPurchase contourLines;
protected InAppSubscription monthlyLiveUpdates;
protected InAppSubscription discountedMonthlyLiveUpdates;
protected InAppSubscriptionList liveUpdates;
protected InAppPurchase[] inAppPurchases;
private static final InAppSubscription[] LIVE_UPDATES_FULL = new InAppSubscription[]{
new InAppPurchaseLiveUpdatesOldMonthlyFull(),
new InAppPurchaseLiveUpdatesMonthlyFull(),
new InAppPurchaseLiveUpdates3MonthsFull(),
new InAppPurchaseLiveUpdatesAnnualFull()
};
private static final InAppSubscription[] LIVE_UPDATES_FREE = new InAppSubscription[]{
new InAppPurchaseLiveUpdatesOldMonthlyFree(),
new InAppPurchaseLiveUpdatesMonthlyFree(),
new InAppPurchaseLiveUpdates3MonthsFree(),
new InAppPurchaseLiveUpdatesAnnualFree()
};
private InAppPurchase fullVersion;
private InAppPurchase depthContours;
private InAppPurchase contourLines;
private InAppSubscription monthlyLiveUpdates;
private InAppSubscription discountedMonthlyLiveUpdates;
private InAppSubscriptionList liveUpdates;
private InAppPurchase[] inAppPurchases;
InAppPurchases(OsmandApplication ctx) {
fullVersion = FULL_VERSION;
if (Version.isFreeVersion(ctx)) {
liveUpdates = new LiveUpdatesInAppPurchasesFree();
} else {
liveUpdates = new LiveUpdatesInAppPurchasesFull();
}
for (InAppSubscription s : liveUpdates.getAllSubscriptions()) {
if (s instanceof InAppPurchaseLiveUpdatesMonthly) {
if (s.isDiscounted()) {
discountedMonthlyLiveUpdates = s;
} else {
monthlyLiveUpdates = s;
}
}
}
if (Version.isFreeVersion(ctx)) {
depthContours = DEPTH_CONTOURS_FREE;
} else {
depthContours = DEPTH_CONTOURS_FULL;
}
if (Version.isFreeVersion(ctx)) {
contourLines = CONTOUR_LINES_FREE;
} else {
contourLines = CONTOUR_LINES_FULL;
}
inAppPurchases = new InAppPurchase[] { fullVersion, depthContours, contourLines };
protected InAppPurchases(OsmandApplication ctx) {
}
public InAppPurchase getFullVersion() {
@ -123,7 +73,7 @@ public class InAppPurchases {
public InAppSubscription getPurchasedMonthlyLiveUpdates() {
if (monthlyLiveUpdates.isAnyPurchased()) {
return monthlyLiveUpdates;
} else if (discountedMonthlyLiveUpdates.isAnyPurchased()) {
} else if (discountedMonthlyLiveUpdates != null && discountedMonthlyLiveUpdates.isAnyPurchased()) {
return discountedMonthlyLiveUpdates;
}
return null;
@ -158,31 +108,13 @@ public class InAppPurchases {
return null;
}
public boolean isFullVersion(String sku) {
return FULL_VERSION.getSku().equals(sku);
}
public abstract boolean isFullVersion(String sku);
public boolean isDepthContours(String sku) {
return DEPTH_CONTOURS_FULL.getSku().equals(sku) || DEPTH_CONTOURS_FREE.getSku().equals(sku);
}
public abstract boolean isDepthContours(String sku);
public boolean isContourLines(String sku) {
return CONTOUR_LINES_FULL.getSku().equals(sku) || CONTOUR_LINES_FREE.getSku().equals(sku);
}
public abstract boolean isContourLines(String sku);
public boolean isLiveUpdates(String sku) {
for (InAppPurchase p : LIVE_UPDATES_FULL) {
if (p.getSku().equals(sku)) {
return true;
}
}
for (InAppPurchase p : LIVE_UPDATES_FREE) {
if (p.getSku().equals(sku)) {
return true;
}
}
return false;
}
public abstract boolean isLiveUpdates(String sku);
public abstract static class InAppSubscriptionList {
@ -260,20 +192,6 @@ public class InAppPurchases {
}
}
public static class LiveUpdatesInAppPurchasesFree extends InAppSubscriptionList {
public LiveUpdatesInAppPurchasesFree() {
super(LIVE_UPDATES_FREE);
}
}
public static class LiveUpdatesInAppPurchasesFull extends InAppSubscriptionList {
public LiveUpdatesInAppPurchasesFull() {
super(LIVE_UPDATES_FULL);
}
}
public abstract static class InAppPurchase {
public enum PurchaseState {
@ -295,11 +213,11 @@ public class InAppPurchases {
private NumberFormat currencyFormatter;
private InAppPurchase(@NonNull String sku) {
protected InAppPurchase(@NonNull String sku) {
this.sku = sku;
}
private InAppPurchase(@NonNull String sku, boolean discounted) {
protected InAppPurchase(@NonNull String sku, boolean discounted) {
this(sku);
this.discounted = discounted;
}
@ -777,23 +695,9 @@ public class InAppPurchases {
}
}
public static class InAppPurchaseFullVersion extends InAppPurchase {
private static final String SKU_FULL_VERSION_PRICE = "osmand_full_version_price";
InAppPurchaseFullVersion() {
super(SKU_FULL_VERSION_PRICE);
}
@Override
public String getDefaultPrice(Context ctx) {
return ctx.getString(R.string.full_version_price);
}
}
public static class InAppPurchaseDepthContours extends InAppPurchase {
private InAppPurchaseDepthContours(String sku) {
protected InAppPurchaseDepthContours(String sku) {
super(sku);
}
@ -803,27 +707,9 @@ public class InAppPurchases {
}
}
public static class InAppPurchaseDepthContoursFull extends InAppPurchaseDepthContours {
private static final String SKU_DEPTH_CONTOURS_FULL = "net.osmand.seadepth_plus";
InAppPurchaseDepthContoursFull() {
super(SKU_DEPTH_CONTOURS_FULL);
}
}
public static class InAppPurchaseDepthContoursFree extends InAppPurchaseDepthContours {
private static final String SKU_DEPTH_CONTOURS_FREE = "net.osmand.seadepth";
InAppPurchaseDepthContoursFree() {
super(SKU_DEPTH_CONTOURS_FREE);
}
}
public static class InAppPurchaseContourLines extends InAppPurchase {
private InAppPurchaseContourLines(String sku) {
protected InAppPurchaseContourLines(String sku) {
super(sku);
}
@ -833,25 +719,7 @@ public class InAppPurchases {
}
}
public static class InAppPurchaseContourLinesFull extends InAppPurchaseContourLines {
private static final String SKU_CONTOUR_LINES_FULL = "net.osmand.contourlines_plus";
InAppPurchaseContourLinesFull() {
super(SKU_CONTOUR_LINES_FULL);
}
}
public static class InAppPurchaseContourLinesFree extends InAppPurchaseContourLines {
private static final String SKU_CONTOUR_LINES_FREE = "net.osmand.contourlines";
InAppPurchaseContourLinesFree() {
super(SKU_CONTOUR_LINES_FREE);
}
}
public static abstract class InAppPurchaseLiveUpdatesMonthly extends InAppSubscription {
protected static abstract class InAppPurchaseLiveUpdatesMonthly extends InAppSubscription {
InAppPurchaseLiveUpdatesMonthly(String skuNoVersion, int version) {
super(skuNoVersion, version);
@ -905,45 +773,7 @@ public class InAppPurchases {
}
}
public static class InAppPurchaseLiveUpdatesMonthlyFull extends InAppPurchaseLiveUpdatesMonthly {
private static final String SKU_LIVE_UPDATES_MONTHLY_FULL = "osm_live_subscription_monthly_full";
InAppPurchaseLiveUpdatesMonthlyFull() {
super(SKU_LIVE_UPDATES_MONTHLY_FULL, 1);
}
private InAppPurchaseLiveUpdatesMonthlyFull(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFull(sku) : null;
}
}
public static class InAppPurchaseLiveUpdatesMonthlyFree extends InAppPurchaseLiveUpdatesMonthly {
private static final String SKU_LIVE_UPDATES_MONTHLY_FREE = "osm_live_subscription_monthly_free";
InAppPurchaseLiveUpdatesMonthlyFree() {
super(SKU_LIVE_UPDATES_MONTHLY_FREE, 1);
}
private InAppPurchaseLiveUpdatesMonthlyFree(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFree(sku) : null;
}
}
public static abstract class InAppPurchaseLiveUpdates3Months extends InAppSubscription {
protected static abstract class InAppPurchaseLiveUpdates3Months extends InAppSubscription {
InAppPurchaseLiveUpdates3Months(String skuNoVersion, int version) {
super(skuNoVersion, version);
@ -986,45 +816,7 @@ public class InAppPurchases {
}
}
public static class InAppPurchaseLiveUpdates3MonthsFull extends InAppPurchaseLiveUpdates3Months {
private static final String SKU_LIVE_UPDATES_3_MONTHS_FULL = "osm_live_subscription_3_months_full";
InAppPurchaseLiveUpdates3MonthsFull() {
super(SKU_LIVE_UPDATES_3_MONTHS_FULL, 1);
}
private InAppPurchaseLiveUpdates3MonthsFull(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFull(sku) : null;
}
}
public static class InAppPurchaseLiveUpdates3MonthsFree extends InAppPurchaseLiveUpdates3Months {
private static final String SKU_LIVE_UPDATES_3_MONTHS_FREE = "osm_live_subscription_3_months_free";
InAppPurchaseLiveUpdates3MonthsFree() {
super(SKU_LIVE_UPDATES_3_MONTHS_FREE, 1);
}
private InAppPurchaseLiveUpdates3MonthsFree(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFree(sku) : null;
}
}
public static abstract class InAppPurchaseLiveUpdatesAnnual extends InAppSubscription {
protected static abstract class InAppPurchaseLiveUpdatesAnnual extends InAppSubscription {
InAppPurchaseLiveUpdatesAnnual(String skuNoVersion, int version) {
super(skuNoVersion, version);
@ -1067,44 +859,6 @@ public class InAppPurchases {
}
}
public static class InAppPurchaseLiveUpdatesAnnualFull extends InAppPurchaseLiveUpdatesAnnual {
private static final String SKU_LIVE_UPDATES_ANNUAL_FULL = "osm_live_subscription_annual_full";
InAppPurchaseLiveUpdatesAnnualFull() {
super(SKU_LIVE_UPDATES_ANNUAL_FULL, 1);
}
private InAppPurchaseLiveUpdatesAnnualFull(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFull(sku) : null;
}
}
public static class InAppPurchaseLiveUpdatesAnnualFree extends InAppPurchaseLiveUpdatesAnnual {
private static final String SKU_LIVE_UPDATES_ANNUAL_FREE = "osm_live_subscription_annual_free";
InAppPurchaseLiveUpdatesAnnualFree() {
super(SKU_LIVE_UPDATES_ANNUAL_FREE, 1);
}
private InAppPurchaseLiveUpdatesAnnualFree(@NonNull String sku) {
super(sku);
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFree(sku) : null;
}
}
public static class InAppPurchaseLiveUpdatesOldMonthly extends InAppPurchaseLiveUpdatesMonthly {
InAppPurchaseLiveUpdatesOldMonthly(String sku) {
@ -1127,54 +881,5 @@ public class InAppPurchases {
return null;
}
}
public static class InAppPurchaseLiveUpdatesOldMonthlyFull extends InAppPurchaseLiveUpdatesOldMonthly {
private static final String SKU_LIVE_UPDATES_OLD_MONTHLY_FULL = "osm_live_subscription_2";
InAppPurchaseLiveUpdatesOldMonthlyFull() {
super(SKU_LIVE_UPDATES_OLD_MONTHLY_FULL);
}
}
public static class InAppPurchaseLiveUpdatesOldMonthlyFree extends InAppPurchaseLiveUpdatesOldMonthly {
private static final String SKU_LIVE_UPDATES_OLD_MONTHLY_FREE = "osm_free_live_subscription_2";
InAppPurchaseLiveUpdatesOldMonthlyFree() {
super(SKU_LIVE_UPDATES_OLD_MONTHLY_FREE);
}
}
public static class InAppPurchaseLiveUpdatesOldSubscription extends InAppSubscription {
private SkuDetails details;
InAppPurchaseLiveUpdatesOldSubscription(@NonNull SkuDetails details) {
super(details.getSku(), true);
this.details = details;
}
@Override
public String getDefaultPrice(Context ctx) {
return "";
}
@Override
public CharSequence getTitle(Context ctx) {
return details.getTitle();
}
@Override
public CharSequence getDescription(@NonNull Context ctx) {
return details.getDescription();
}
@Nullable
@Override
protected InAppSubscription newInstance(@NonNull String sku) {
return null;
}
}
}

View file

@ -2008,6 +2008,7 @@ public class OsmandSettings {
public final OsmandPreference<Boolean> LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN = new BooleanPreference("live_updates_purchase_cancelled_second_dlg_shown", false).makeGlobal();
public final OsmandPreference<Boolean> FULL_VERSION_PURCHASED = new BooleanPreference("billing_full_version_purchased", false).makeGlobal();
public final OsmandPreference<Boolean> DEPTH_CONTOURS_PURCHASED = new BooleanPreference("billing_sea_depth_purchased", false).makeGlobal();
public final OsmandPreference<Boolean> CONTOUR_LINES_PURCHASED = new BooleanPreference("billing_srtm_purchased", false).makeGlobal();
public final OsmandPreference<Boolean> EMAIL_SUBSCRIBED = new BooleanPreference("email_subscribed", false).makeGlobal();
public final OsmandPreference<Integer> DISCOUNT_ID = new IntPreference("discount_id", 0).makeGlobal();

View file

@ -95,7 +95,9 @@ public class SRTMPlugin extends OsmandPlugin {
@Override
protected boolean pluginAvailable(OsmandApplication app) {
return super.pluginAvailable(app) || InAppPurchaseHelper.isSubscribedToLiveUpdates(app);
return super.pluginAvailable(app)
|| InAppPurchaseHelper.isSubscribedToLiveUpdates(app)
|| InAppPurchaseHelper.isContourLinesPurchased(app);
}
@Override

View file

@ -4,6 +4,14 @@ buildscript {
google()
mavenCentral()
jcenter()
maven {
url 'https://developer.huawei.com/repo/'
content {
includeGroup 'com.huawei.agconnect'
includeGroup 'com.huawei.hms'
includeGroup 'com.huawei.hmf'
}
}
}
dependencies {
//classpath 'com.android.tools.build:gradle:2.+'
@ -11,6 +19,9 @@ buildscript {
classpath 'com.google.gms:google-services:3.0.0'
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
if (gradle.startParameter.taskNames.toString().contains("huawei")) {
classpath 'com.huawei.agconnect:agcp:1.4.1.300'
}
}
}
@ -32,5 +43,13 @@ allprojects {
maven {
url "https://jitpack.io"
}
maven {
url 'https://developer.huawei.com/repo/'
content {
includeGroup 'com.huawei.agconnect'
includeGroup 'com.huawei.hms'
includeGroup 'com.huawei.hmf'
}
}
}
}