diff --git a/.gitignore b/.gitignore index 5798e73f27..33e746a3d6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,10 @@ OsmAndCore_*.aar .project out/ +# Huawei +agconnect-services.json +OsmAndHms.jks + # Android Studio /.idea *.iml diff --git a/OsmAnd/.gitignore b/OsmAnd/.gitignore index e3071e5fbf..8bfbd5b688 100644 --- a/OsmAnd/.gitignore +++ b/OsmAnd/.gitignore @@ -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_* diff --git a/OsmAnd/AndroidManifest-freehuawei.xml b/OsmAnd/AndroidManifest-freehuawei.xml index e96bb7fe47..c234628537 100644 --- a/OsmAnd/AndroidManifest-freehuawei.xml +++ b/OsmAnd/AndroidManifest-freehuawei.xml @@ -2,24 +2,24 @@ - - - - + + + + + - + tools:replace="android:authorities" + android:authorities="net.osmand.huawei.fileprovider"/> - \ No newline at end of file diff --git a/OsmAnd/AndroidManifest-huawei.xml b/OsmAnd/AndroidManifest-huawei.xml deleted file mode 100644 index bc847980cd..0000000000 --- a/OsmAnd/AndroidManifest-huawei.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/OsmAnd/build.gradle b/OsmAnd/build.gradle index 91b0b8022f..a87a05b117 100644 --- a/OsmAnd/build.gradle +++ b/OsmAnd/build.gradle @@ -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" } @@ -191,10 +202,6 @@ android { resConfig "en" //resConfigs "xxhdpi", "nodpi" } - huawei { - dimension "version" - applicationId "net.osmand.plus.huawei" - } freehuawei { dimension "version" applicationId "net.osmand.huawei" @@ -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,46 +286,14 @@ 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) { from "../../resources/voice" into "assets/voice" @@ -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' } diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index 086a7470d5..b58b6d0a1f 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -11,6 +11,7 @@ Thx - Hardy --> + Thank you for purchasing \'Contour lines\' Start/finish icons Name: A – Z Name: Z – A diff --git a/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java new file mode 100644 index 0000000000..c6925a5e63 --- /dev/null +++ b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java @@ -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 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 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 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 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 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 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 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 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 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 getAllOwnedSubscriptionSkus() { + List 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 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 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 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 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 tokensToSend = new ArrayList<>(); + if (liveUpdatesPurchases.size() > 0) { + List 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 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, 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; + } + } +} diff --git a/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchasesImpl.java b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchasesImpl.java new file mode 100644 index 0000000000..a2a0f8d680 --- /dev/null +++ b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchasesImpl.java @@ -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); + } + } +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/CipherUtil.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/CipherUtil.java new file mode 100755 index 0000000000..c7e6cd11be --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/CipherUtil.java @@ -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; + } + +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/Constants.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/Constants.java new file mode 100755 index 0000000000..fba4db210a --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/Constants.java @@ -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; + +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/ExceptionHandle.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/ExceptionHandle.java new file mode 100755 index 0000000000..06fa78b260 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/ExceptionHandle.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/IapApiCallback.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/IapApiCallback.java new file mode 100755 index 0000000000..d8fb908093 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/IapApiCallback.java @@ -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 { + + /** + * 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); +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/IapRequestHelper.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/IapRequestHelper.java new file mode 100755 index 0000000000..5c1b7838a5 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/IapRequestHelper.java @@ -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 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 task = mClient.isEnvReady(); + task.addOnSuccessListener(new OnSuccessListener() { + @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 productIds, int type, final IapApiCallback callback) { + Log.i(TAG, "call obtainProductInfo"); + + Task task = iapClient.obtainProductInfo(createProductInfoReq(type, productIds)); + task.addOnSuccessListener(new OnSuccessListener() { + @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 task = iapClient.createPurchaseIntent(createPurchaseIntentReq(type, productId)); + task.addOnSuccessListener(new OnSuccessListener() { + @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 task = iapClient.createPurchaseIntent(req); + task.addOnSuccessListener(new OnSuccessListener() { + @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.
+ * 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 task = mClient.obtainOwnedPurchases(IapRequestHelper.createOwnedPurchasesReq(type, continuationToken)); + task.addOnSuccessListener(new OnSuccessListener() { + @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 task = iapClient.obtainOwnedPurchaseRecord(createOwnedPurchasesReq(priceType, continuationToken)); + task.addOnSuccessListener(new OnSuccessListener() { + @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 task = iapClient.consumeOwnedPurchase(createConsumeOwnedPurchaseReq(purchaseToken)); + task.addOnSuccessListener(new OnSuccessListener() { + @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 task = iapClient.startIapActivity(req); + + task.addOnSuccessListener(new OnSuccessListener() { + @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); + } + }); + } + +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java new file mode 100644 index 0000000000..c3d6fc193c --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java @@ -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 productInfos; + private OwnedPurchasesResult ownedSubscriptions; + private List 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 activityRef = new WeakReference<>(activity); + IapRequestHelper.isEnvReady(mClient, new IapApiCallback() { + + 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() { + @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 task = getIapClient().startIapActivity(req); + task.addOnSuccessListener(new OnSuccessListener() { + @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 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, 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() { + @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() { + @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() { + @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 productIds = new HashSet<>(); + List 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() { + @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 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() { + @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 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 liveUpdatesPurchases = new ArrayList<>(); + for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) { + InAppPurchaseData purchaseData = getPurchaseData(p.getSku()); + if (purchaseData != null) { + liveUpdatesPurchases.add(purchaseData); + if (!subscribedToLiveUpdates) { + subscribedToLiveUpdates = true; + } + } + } + OsmandPreference 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 tokensToSend = new ArrayList<>(); + if (liveUpdatesPurchases.size() > 0) { + List 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 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; + } +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchasesImpl.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchasesImpl.java new file mode 100644 index 0000000000..4ed0021b6f --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchasesImpl.java @@ -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); + } + } +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppUtils.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppUtils.java new file mode 100644 index 0000000000..445727de96 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppUtils.java @@ -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; + } +} \ No newline at end of file diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/SubscriptionUtils.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/SubscriptionUtils.java new file mode 100755 index 0000000000..3b249cb300 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/SubscriptionUtils.java @@ -0,0 +1,138 @@ +/** + * 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.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 dataList = result.getInAppPurchaseDataList(); + List 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; + } +} diff --git a/OsmAnd/src/net/osmand/plus/AppInitializer.java b/OsmAnd/src/net/osmand/plus/AppInitializer.java index 2837bff6f8..0ae24b7c5f 100644 --- a/OsmAnd/src/net/osmand/plus/AppInitializer.java +++ b/OsmAnd/src/net/osmand/plus/AppInitializer.java @@ -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); diff --git a/OsmAnd/src/net/osmand/plus/HuaweiDrmHelper.java b/OsmAnd/src/net/osmand/plus/HuaweiDrmHelper.java deleted file mode 100644 index 7cc2f2798e..0000000000 --- a/OsmAnd/src/net/osmand/plus/HuaweiDrmHelper.java +++ /dev/null @@ -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 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); - } -} diff --git a/OsmAnd/src/net/osmand/plus/Version.java b/OsmAnd/src/net/osmand/plus/Version.java index 14ed68b100..86d26ec954 100644 --- a/OsmAnd/src/net/osmand/plus/Version.java +++ b/OsmAnd/src/net/osmand/plus/Version.java @@ -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) { diff --git a/OsmAnd/src/net/osmand/plus/activities/MapActivity.java b/OsmAnd/src/net/osmand/plus/activities/MapActivity.java index 2816e7d5b0..3baa33cb32 100644 --- a/OsmAnd/src/net/osmand/plus/activities/MapActivity.java +++ b/OsmAnd/src/net/osmand/plus/activities/MapActivity.java @@ -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); diff --git a/OsmAnd/src/net/osmand/plus/activities/MapActivityActions.java b/OsmAnd/src/net/osmand/plus/activities/MapActivityActions.java index dbd0f133fa..1d6e2c2dc7 100644 --- a/OsmAnd/src/net/osmand/plus/activities/MapActivityActions.java +++ b/OsmAnd/src/net/osmand/plus/activities/MapActivityActions.java @@ -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) diff --git a/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java b/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java index cb91f7d167..47f7a17444 100644 --- a/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java +++ b/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java @@ -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,17 +49,39 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In protected void onDestroy() { super.onDestroy(); deinitInAppPurchaseHelper(); + activityDestroyed = true; } private void initInAppPurchaseHelper() { deinitInAppPurchaseHelper(); - - if (purchaseHelper != null) { - purchaseHelper.setUiActivity(this); - if (purchaseHelper.needRequestInventory()) { - purchaseHelper.requestInventory(); + if (purchaseHelper == null) { + OsmandApplication app = getMyApplication(); + InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper(); + if (app.getSettings().isInternetConnectionAvailable() + && isInAppPurchaseAllowed() + && isInAppPurchaseSupported(purchaseHelper)) { + this.purchaseHelper = purchaseHelper; } } + if (purchaseHelper != null) { + final WeakReference 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() { @@ -80,7 +98,11 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper(); if (purchaseHelper != null) { app.logEvent("in_app_purchase_redirect"); - purchaseHelper.purchaseFullVersion(activity); + 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"); - purchaseHelper.purchaseDepthContours(activity); + 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 } diff --git a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java index 1e3b650a8f..c909d50845 100644 --- a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java +++ b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java @@ -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; diff --git a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanHillshadeSrtmDialogFragment.java b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanHillshadeSrtmDialogFragment.java index b2858b53b8..d3a48a5a15 100644 --- a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanHillshadeSrtmDialogFragment.java +++ b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanHillshadeSrtmDialogFragment.java @@ -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(); } } diff --git a/OsmAnd/src/net/osmand/plus/helpers/DiscountHelper.java b/OsmAnd/src/net/osmand/plus/helpers/DiscountHelper.java index 61ae82399f..a4bb6ec74f 100644 --- a/OsmAnd/src/net/osmand/plus/helpers/DiscountHelper.java +++ b/OsmAnd/src/net/osmand/plus/helpers/DiscountHelper.java @@ -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"); - purchaseHelper.purchaseFullVersion(mapActivity); + 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())) { diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java index ac7e2c77fb..1364d7b154 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java @@ -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 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; - } - - 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); + public void onCommandDone(@NonNull InAppCommand command) { + processingTask = false; } - - @Override - public void onConsumeFinished(String token, BillingResult billingResult) { - } - - @Override - public void onPurchasesUpdated(final List 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 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 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 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 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 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 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 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 getAllOwnedSubscriptionSkus() { - List 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 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 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 liveUpdatesPurchases = new ArrayList<>(); - for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) { - Purchase purchase = getPurchase(p.getSku()); - if (purchase != null) { - liveUpdatesPurchases.add(purchase); - if (!subscribedToLiveUpdates) { - subscribedToLiveUpdates = true; - } - } - } - OsmandPreference 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 tokensToSend = new ArrayList<>(); - if (liveUpdatesPurchases.size() > 0) { - List 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 { @@ -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, + final String sku, final String payload) throws UnsupportedOperationException; + @SuppressLint("StaticFieldLeak") private class RequestInventoryTask extends AsyncTask { @@ -808,38 +402,41 @@ public class InAppPurchaseHelper { try { JSONObject obj = new JSONObject(response); JSONArray names = obj.names(); - for (int i = 0; i < names.length(); i++) { - String skuType = names.getString(i); - JSONObject subObj = obj.getJSONObject(skuType); - String sku = subObj.getString("sku"); - if (!Algorithms.isEmpty(sku)) { - getLiveUpdates().upgradeSubscription(sku); + if (names != null) { + for (int i = 0; i < names.length(); i++) { + String skuType = names.getString(i); + JSONObject subObj = obj.getJSONObject(skuType); + String sku = subObj.getString("sku"); + if (!Algorithms.isEmpty(sku)) { + getLiveUpdates().upgradeSubscription(sku); + } } } } catch (JSONException e) { logError("Json parsing error", e); } } - exec(InAppPurchaseTaskType.REQUEST_INVENTORY, new InAppRunnable() { - @Override - public boolean run(InAppPurchaseHelper helper) { - logDebug("Setup successful. Querying inventory."); - try { - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { - billingManager.queryPurchases(); - } else { - throw new IllegalStateException("BillingManager disposed"); - } - return false; - } catch (Exception e) { - logError("queryInventoryAsync Error", e); - notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY); - stop(true); - } - return true; - } - }); + exec(InAppPurchaseTaskType.REQUEST_INVENTORY, getRequestInventoryCommand()); + } + } + + protected abstract InAppCommand getRequestInventoryCommand() throws UnsupportedOperationException; + + protected void onSkuDetailsResponseDone(List purchaseInfoList) { + final AndroidNetworkUtils.OnRequestResultListener listener = new AndroidNetworkUtils.OnRequestResultListener() { + @Override + public void onResult(String result) { + notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY); + notifyGetItems(); + stop(true); + logDebug("Initial inapp query finished"); + } + }; + + 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 purchases, final OnRequestResultListener listener) { + protected void sendTokens(@NonNull final List 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 requests = new ArrayList<>(); - for (Purchase purchase : purchases) { + for (PurchaseInfo info : purchaseInfoList) { Map 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 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 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); } diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java index b42b57f045..5004e97165 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java @@ -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; - } - } } diff --git a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java index efa7147e6c..1619102c84 100644 --- a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java +++ b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java @@ -2008,6 +2008,7 @@ public class OsmandSettings { public final OsmandPreference LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN = new BooleanPreference("live_updates_purchase_cancelled_second_dlg_shown", false).makeGlobal(); public final OsmandPreference FULL_VERSION_PURCHASED = new BooleanPreference("billing_full_version_purchased", false).makeGlobal(); public final OsmandPreference DEPTH_CONTOURS_PURCHASED = new BooleanPreference("billing_sea_depth_purchased", false).makeGlobal(); + public final OsmandPreference CONTOUR_LINES_PURCHASED = new BooleanPreference("billing_srtm_purchased", false).makeGlobal(); public final OsmandPreference EMAIL_SUBSCRIBED = new BooleanPreference("email_subscribed", false).makeGlobal(); public final OsmandPreference DISCOUNT_ID = new IntPreference("discount_id", 0).makeGlobal(); diff --git a/OsmAnd/src/net/osmand/plus/srtmplugin/SRTMPlugin.java b/OsmAnd/src/net/osmand/plus/srtmplugin/SRTMPlugin.java index 9138e9879d..3690afbc3d 100644 --- a/OsmAnd/src/net/osmand/plus/srtmplugin/SRTMPlugin.java +++ b/OsmAnd/src/net/osmand/plus/srtmplugin/SRTMPlugin.java @@ -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 diff --git a/build.gradle b/build.gradle index 1cd0b221b4..e75ca7d5b8 100644 --- a/build.gradle +++ b/build.gradle @@ -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' + } + } } }