diff --git a/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java index c48294aeb2..fc9b1056f6 100644 --- a/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java +++ b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java @@ -13,6 +13,8 @@ import com.android.billingclient.api.SkuDetailsResponseListener; import net.osmand.AndroidUtils; import net.osmand.plus.OsmandApplication; +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.util.Algorithms; @@ -49,13 +51,21 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { 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 InAppRunnable runnable) { + protected void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppCommand runnable) { billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingManager.BillingUpdatesListener() { @Override @@ -77,7 +87,7 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { return; } - processingTask = !runnable.run(InAppPurchaseHelperImpl.this); + runnable.run(InAppPurchaseHelperImpl.this); } @Override @@ -114,7 +124,7 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { } List skuSubscriptions = new ArrayList<>(); - for (InAppPurchases.InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) { + for (InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) { skuSubscriptions.add(subscription.getSku()); } for (Purchase p : purchases) { @@ -165,9 +175,9 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { public void purchaseFullVersion(final Activity activity) { notifyShowProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION); - exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, new InAppRunnable() { + exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, new InAppCommand() { @Override - public boolean run(InAppPurchaseHelper helper) { + public void run(InAppPurchaseHelper helper) { try { SkuDetails skuDetails = getSkuDetails(getFullVersion().getSku()); if (skuDetails == null) { @@ -179,22 +189,21 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { } else { throw new IllegalStateException("BillingManager disposed"); } - return false; + commandDone(); } catch (Exception e) { complain("Cannot launch full version purchase!"); logError("purchaseFullVersion Error", e); stop(true); } - return true; } }); } public void purchaseDepthContours(final Activity activity) { notifyShowProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS); - exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, new InAppRunnable() { + exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, new InAppCommand() { @Override - public boolean run(InAppPurchaseHelper helper) { + public void run(InAppPurchaseHelper helper) { try { SkuDetails skuDetails = getSkuDetails(getDepthContours().getSku()); if (skuDetails == null) { @@ -206,13 +215,12 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { } else { throw new IllegalStateException("BillingManager disposed"); } - return false; + commandDone(); } catch (Exception e) { complain("Cannot launch depth contours purchase!"); logError("purchaseDepthContours Error", e); stop(true); } - return true; } }); } @@ -295,7 +303,7 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { */ List allOwnedSubscriptionSkus = getAllOwnedSubscriptionSkus(); - for (InAppPurchases.InAppSubscription s : getLiveUpdates().getAllSubscriptions()) { + for (InAppSubscription s : getLiveUpdates().getAllSubscriptions()) { if (hasDetails(s.getSku())) { Purchase purchase = getPurchase(s.getSku()); SkuDetails liveUpdatesDetails = getSkuDetails(s.getSku()); @@ -309,9 +317,9 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { Purchase purchase = getPurchase(sku); SkuDetails liveUpdatesDetails = getSkuDetails(sku); if (liveUpdatesDetails != null) { - InAppPurchases.InAppSubscription s = getLiveUpdates().upgradeSubscription(sku); + InAppSubscription s = getLiveUpdates().upgradeSubscription(sku); if (s == null) { - s = new InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails); + s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails); } fetchInAppPurchase(s, liveUpdatesDetails, purchase); } @@ -441,21 +449,21 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { } String subscriptionPeriod = skuDetails.getSubscriptionPeriod(); if (!Algorithms.isEmpty(subscriptionPeriod)) { - if (inAppPurchase instanceof InAppPurchases.InAppSubscription) { + if (inAppPurchase instanceof InAppSubscription) { try { - ((InAppPurchases.InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod); + ((InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod); } catch (ParseException e) { LOG.error(e); } } } - if (inAppPurchase instanceof InAppPurchases.InAppSubscription) { + if (inAppPurchase instanceof InAppSubscription) { String introductoryPrice = skuDetails.getIntroductoryPrice(); String introductoryPricePeriod = skuDetails.getIntroductoryPricePeriod(); String introductoryPriceCycles = skuDetails.getIntroductoryPriceCycles(); long introductoryPriceAmountMicros = skuDetails.getIntroductoryPriceAmountMicros(); if (!Algorithms.isEmpty(introductoryPrice)) { - InAppPurchases.InAppSubscription s = (InAppPurchases.InAppSubscription) inAppPurchase; + InAppSubscription s = (InAppSubscription) inAppPurchase; try { s.setIntroductoryInfo(new InAppPurchases.InAppSubscriptionIntroductoryInfo(s, introductoryPrice, introductoryPriceAmountMicros, introductoryPricePeriod, introductoryPriceCycles)); @@ -466,10 +474,10 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { } } - protected InAppRunnable getPurchaseLiveUpdatesCommand(final WeakReference activity, final String sku, final String payload) { - return new InAppRunnable() { + protected InAppCommand getPurchaseLiveUpdatesCommand(final WeakReference activity, final String sku, final String payload) { + return new InAppCommand() { @Override - public boolean run(InAppPurchaseHelper helper) { + public void run(InAppPurchaseHelper helper) { try { Activity a = activity.get(); SkuDetails skuDetails = getSkuDetails(sku); @@ -481,7 +489,7 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { } else { throw new IllegalStateException("BillingManager disposed"); } - return false; + commandDone(); } else { stop(true); } @@ -489,15 +497,14 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { logError("launchPurchaseFlow Error", e); stop(true); } - return true; } }; } - protected InAppRunnable getRequestInventoryCommand() { - return new InAppRunnable() { + protected InAppCommand getRequestInventoryCommand() { + return new InAppCommand() { @Override - public boolean run(InAppPurchaseHelper helper) { + public void run(InAppPurchaseHelper helper) { logDebug("Setup successful. Querying inventory."); try { BillingManager billingManager = getBillingManager(); @@ -506,13 +513,12 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { } else { throw new IllegalStateException("BillingManager disposed"); } - return false; + commandDone(); } catch (Exception e) { logError("queryInventoryAsync Error", e); notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY); stop(true); } - return true; } }; } 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..2bfef4b76b --- /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 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAooen3X9jSWarxugznzzMSvp4zir1Pg6uPOm7fqlLOL0Ix52e5FpeotMx871pQ9hrCkiyFg2e6UxD8IXXjvK6QJQbjNJ2jIfKkCusm90yloSEfvyLeiq5y7zg4+DoPglHi8RxZ9y308YIqnRDoslfGm5DnWa8RKUvFRVRiu1p3FN4SYIa/FWLtS5yygemtqMJi8I14V7xqQ5wExCGeSA6j1/AAWXEwZncJwKn0BTXQSvwVBPBRM5ksgt4q+Sc484ZIbntATyxsUipnEBFxq1OXn5Zw5/vVxUC8RSyDMQ/kC2RaEcFtA1tlIIjIdurbpNg3tyViPfQUQndvOs4nDrFzwIDAQAB"; + + /** + * 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 index a7e8589f33..61de5125c0 100644 --- a/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java @@ -1,393 +1,229 @@ 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.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 net.osmand.AndroidUtils; import net.osmand.plus.OsmandApplication; -import net.osmand.plus.inapp.util.BillingManager; +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.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 { - // The helper object - private BillingManager billingManager; - private List skuDetailsList; + 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); } - protected void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppRunnable runnable) { - billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingManager.BillingUpdatesListener() { - - @Override - public void onBillingClientSetupFinished() { - logDebug("Setup finished."); - - BillingManager billingManager = getBillingManager(); - // Have we been disposed of in the meantime? If so, quit. - if (billingManager == null) { - stop(true); - return; + @Override + public void isInAppPurchaseSupported(@NonNull final Activity activity, @Nullable final InAppPurchaseInitCallback callback) { + if (envReady) { + if (callback != null) { + if (purchaseSupported) { + callback.onSuccess(); + } else { + callback.onFail(); } - - if (!billingManager.isServiceConnected()) { - // Oh noes, there was a problem. - //complain("Problem setting up in-app billing: " + result); - notifyError(taskType, billingManager.getBillingClientResponseMessage()); - stop(true); - return; - } - - processingTask = !runnable.run(InAppPurchaseHelperImpl.this); } + } 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() { - @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; + private void onReady(boolean succeed) { + logDebug("Setup finished."); + envReady = true; + purchaseSupported = succeed; + if (callback != null) { + if (succeed) { + callback.onSuccess(); + } else { + callback.onFail(); + } + } } - if (activeTask == InAppPurchaseTaskType.REQUEST_INVENTORY) { - List skuInApps = new ArrayList<>(); - for (InAppPurchases.InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) { - skuInApps.add(purchase.getSku()); - } - for (Purchase p : purchases) { - skuInApps.add(p.getSku()); - } - billingManager.querySkuDetailsAsync(BillingClient.SkuType.INAPP, skuInApps, new SkuDetailsResponseListener() { - @Override - public void onSkuDetailsResponse(BillingResult billingResult, final List 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; - } + @Override + public void onSuccess(IsEnvReadyResult result) { + onReady(true); + } - List skuSubscriptions = new ArrayList<>(); - for (InAppPurchases.InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) { - skuSubscriptions.add(subscription.getSku()); - } - for (Purchase p : purchases) { - skuSubscriptions.add(p.getSku()); - } + @Override + public void onFail(Exception e) { + onReady(false); + LOG.error("isEnvReady fail, " + e.getMessage(), e); + ExceptionHandle.handle(activityRef.get(), e); + } + }); + } + } - BillingManager billingManager = getBillingManager(); - // Have we been disposed of in the meantime? If so, quit. - if (billingManager == null) { - stop(true); - return; - } + protected void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppCommand command) { + if (envReady) { + command.run(this); + } else { + command.commandDone(); + } + } - billingManager.querySkuDetailsAsync(BillingClient.SkuType.SUBS, skuSubscriptions, new SkuDetailsResponseListener() { + 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); + IapRequestHelper.createPurchaseIntent(getIapClient(), productInfo.getProductId(), + IapClient.PriceType.IN_APP_NONCONSUMABLE, new IapApiCallback() { @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; + 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(); + } - List skuDetailsList = new ArrayList<>(skuDetailsListInApps); - skuDetailsList.addAll(skuDetailsListSubscriptions); - InAppPurchaseHelperImpl.this.skuDetailsList = skuDetailsList; - - mSkuDetailsResponseListener.onSkuDetailsResponse(billingResult, skuDetailsList); + @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(); } }); - } - }); - } - for (Purchase purchase : purchases) { - if (!purchase.isAcknowledged()) { - onPurchaseFinished(purchase); - } + } catch (Exception e) { + complain("Cannot launch full version purchase!"); + logError("purchaseFullVersion Error", e); + stop(true); } } - - @Override - public void onPurchaseCanceled() { - stop(true); - } - }); + }; } @Override - public void purchaseFullVersion(Activity activity) throws UnsupportedOperationException { - throw new UnsupportedOperationException(); + public void purchaseFullVersion(@NonNull final Activity activity) throws UnsupportedOperationException { + notifyShowProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION); + exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, getPurchaseInAppCommand(activity, "")); } @Override - public void purchaseDepthContours(Activity activity) throws UnsupportedOperationException { - throw new UnsupportedOperationException(); + public void purchaseDepthContours(@NonNull final Activity activity) throws UnsupportedOperationException { + notifyShowProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS); + exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, getPurchaseInAppCommand(activity, "")); } @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; + 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 sku) { - return getSkuDetails(sku) != null; + private boolean hasDetails(@NonNull String productId) { + return getProductInfo(productId) != 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; - } + 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 null; + return data; } - // 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 (InAppPurchases.InAppSubscription s : getLiveUpdates().getAllSubscriptions()) { - if (hasDetails(s.getSku())) { - Purchase purchase = getPurchase(s.getSku()); - SkuDetails liveUpdatesDetails = getSkuDetails(s.getSku()); - if (liveUpdatesDetails != null) { - fetchInAppPurchase(s, liveUpdatesDetails, purchase); - } - allOwnedSubscriptionSkus.remove(s.getSku()); - } - } - for (String sku : allOwnedSubscriptionSkus) { - Purchase purchase = getPurchase(sku); - SkuDetails liveUpdatesDetails = getSkuDetails(sku); - if (liveUpdatesDetails != null) { - InAppPurchases.InAppSubscription s = getLiveUpdates().upgradeSubscription(sku); - if (s == null) { - s = new InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails); - } - fetchInAppPurchase(s, liveUpdatesDetails, purchase); - } - } - - InAppPurchases.InAppPurchase fullVersion = getFullVersion(); - if (hasDetails(fullVersion.getSku())) { - Purchase purchase = getPurchase(fullVersion.getSku()); - SkuDetails fullPriceDetails = getSkuDetails(fullVersion.getSku()); - if (fullPriceDetails != null) { - fetchInAppPurchase(fullVersion, fullPriceDetails, purchase); - } - } - - InAppPurchases.InAppPurchase depthContours = getDepthContours(); - if (hasDetails(depthContours.getSku())) { - Purchase purchase = getPurchase(depthContours.getSku()); - SkuDetails depthContoursDetails = getSkuDetails(depthContours.getSku()); - if (depthContoursDetails != null) { - fetchInAppPurchase(depthContours, depthContoursDetails, purchase); - } - } - - InAppPurchases.InAppPurchase contourLines = getContourLines(); - if (hasDetails(contourLines.getSku())) { - Purchase purchase = getPurchase(contourLines.getSku()); - SkuDetails contourLinesDetails = getSkuDetails(contourLines.getSku()); - if (contourLinesDetails != null) { - fetchInAppPurchase(contourLines, contourLinesDetails, purchase); - } - } - - Purchase fullVersionPurchase = getPurchase(fullVersion.getSku()); - boolean fullVersionPurchased = fullVersionPurchase != null; - if (fullVersionPurchased) { - ctx.getSettings().FULL_VERSION_PURCHASED.set(true); - } - - Purchase depthContoursPurchase = getPurchase(depthContours.getSku()); - boolean depthContoursPurchased = depthContoursPurchase != null; - if (depthContoursPurchased) { - ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true); - } - - // Do we have the live updates? - boolean subscribedToLiveUpdates = false; - List liveUpdatesPurchases = new ArrayList<>(); - for (InAppPurchases.InAppPurchase p : getLiveUpdates().getAllSubscriptions()) { - Purchase purchase = getPurchase(p.getSku()); - if (purchase != null) { - liveUpdatesPurchases.add(purchase); - if (!subscribedToLiveUpdates) { - subscribedToLiveUpdates = true; - } - } - } - OsmandSettings.OsmandPreference 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 PurchaseInfo getPurchaseInfo(InAppPurchaseData purchase) { + return new PurchaseInfo(purchase.getProductId(), purchase.getOrderID(), purchase.getPurchaseToken()); } - private void fetchInAppPurchase(@NonNull InAppPurchases.InAppPurchase inAppPurchase, @NonNull SkuDetails skuDetails, @Nullable Purchase purchase) { - if (purchase != null) { - inAppPurchase.setPurchaseState(InAppPurchases.InAppPurchase.PurchaseState.PURCHASED); - inAppPurchase.setPurchaseTime(purchase.getPurchaseTime()); + 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(InAppPurchases.InAppPurchase.PurchaseState.NOT_PURCHASED); + inAppPurchase.setPurchaseState(InAppPurchase.PurchaseState.NOT_PURCHASED); } - inAppPurchase.setPrice(skuDetails.getPrice()); - inAppPurchase.setPriceCurrencyCode(skuDetails.getPriceCurrencyCode()); - if (skuDetails.getPriceAmountMicros() > 0) { - inAppPurchase.setPriceValue(skuDetails.getPriceAmountMicros() / 1000000d); + inAppPurchase.setPrice(productInfo.getPrice()); + inAppPurchase.setPriceCurrencyCode(productInfo.getCurrency()); + if (productInfo.getMicrosPrice() > 0) { + inAppPurchase.setPriceValue(productInfo.getMicrosPrice() / 1000000d); } - String subscriptionPeriod = skuDetails.getSubscriptionPeriod(); + String subscriptionPeriod = productInfo.getSubPeriod(); if (!Algorithms.isEmpty(subscriptionPeriod)) { - if (inAppPurchase instanceof InAppPurchases.InAppSubscription) { + if (inAppPurchase instanceof InAppSubscription) { try { - ((InAppPurchases.InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod); + ((InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod); } catch (ParseException e) { LOG.error(e); } } } - if (inAppPurchase instanceof InAppPurchases.InAppSubscription) { - String introductoryPrice = skuDetails.getIntroductoryPrice(); - String introductoryPricePeriod = skuDetails.getIntroductoryPricePeriod(); - String introductoryPriceCycles = skuDetails.getIntroductoryPriceCycles(); - long introductoryPriceAmountMicros = skuDetails.getIntroductoryPriceAmountMicros(); + if (inAppPurchase instanceof InAppSubscription) { + String introductoryPrice = productInfo.getSubSpecialPrice(); + String introductoryPricePeriod = productInfo.getSubSpecialPeriod(); + int introductoryPriceCycles = productInfo.getSubSpecialPeriodCycles(); + long introductoryPriceAmountMicros = productInfo.getSubSpecialPriceMicros(); if (!Algorithms.isEmpty(introductoryPrice)) { - InAppPurchases.InAppSubscription s = (InAppPurchases.InAppSubscription) inAppPurchase; + InAppSubscription s = (InAppSubscription) inAppPurchase; try { - s.setIntroductoryInfo(new InAppPurchases.InAppSubscriptionIntroductoryInfo(s, introductoryPrice, - introductoryPriceAmountMicros, introductoryPricePeriod, introductoryPriceCycles)); + s.setIntroductoryInfo(new InAppSubscriptionIntroductoryInfo(s, introductoryPrice, + introductoryPriceAmountMicros, introductoryPricePeriod, String.valueOf(introductoryPriceCycles))); } catch (ParseException e) { LOG.error(e); } @@ -395,22 +231,45 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { } } - protected InAppRunnable getPurchaseLiveUpdatesCommand(final WeakReference activity, final String sku, final String payload) { - return new InAppRunnable() { + protected InAppCommand getPurchaseLiveUpdatesCommand(final WeakReference activity, final String sku, final String payload) { + return new InAppCommand() { @Override - public boolean run(InAppPurchaseHelper helper) { + 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"); - } - return false; + 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); } @@ -418,58 +277,362 @@ public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { logError("launchPurchaseFlow Error", e); stop(true); } - return true; } }; } - protected InAppRunnable getRequestInventoryCommand() { - return new InAppRunnable() { + @Override + protected InAppCommand getRequestInventoryCommand() { + return new InAppCommand() { + @Override - public boolean run(InAppPurchaseHelper helper) { + public void run(InAppPurchaseHelper helper) { logDebug("Setup successful. Querying inventory."); try { - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { - billingManager.queryPurchases(); + productInfos = new ArrayList<>(); + if (uiActivity != null) { + obtainOwnedSubscriptions(); } else { - throw new IllegalStateException("BillingManager disposed"); + commandDone(); } - return false; } catch (Exception e) { logError("queryInventoryAsync Error", e); notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY); stop(true); + commandDone(); } - return true; + } + + 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) { + // 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(); + } + }); + + } + + private void obtainSubscriptionsInfo() { + 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) { + logError("obtainSubscriptionsInfo: ProductInfoResult is null"); + commandDone(); + return; + } + 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(); + } + }); + } + + private void obtainInAppsInfo() { + 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) { + logError("obtainInAppsInfo: ProductInfoResult is null"); + commandDone(); + return; + } + productInfos.addAll(result.getProductInfoList()); + + processInventory(); + commandDone(); + } + + @Override + public void onFail(Exception e) { + int errorCode = ExceptionHandle.handle((Activity) uiActivity, e); + if (ExceptionHandle.SOLVED != errorCode) { + LOG.error("Unknown error"); + } + 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); + } + } + + InAppPurchaseData fullVersionPurchase = getPurchaseData(fullVersion.getSku()); + boolean fullVersionPurchased = fullVersionPurchase != null; + if (fullVersionPurchased) { + ctx.getSettings().FULL_VERSION_PURCHASED.set(true); + } + + InAppPurchaseData depthContoursPurchase = getPurchaseData(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()) { + 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(Purchase purchase) { - logDebug("Purchase finished: " + purchase); - - // if we were disposed of in the meantime, quit. - if (getBillingManager() == null) { - stop(true); - return; - } - + private void onPurchaseFinished(InAppPurchaseData purchase) { + logDebug("Purchase finished: " + purchase.getProductId()); onPurchaseDone(getPurchaseInfo(purchase)); } @Override protected boolean isBillingManagerExists() { - return getBillingManager() != null; + return false; } @Override protected void destroyBillingManager() { - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { - billingManager.destroy(); - this.billingManager = null; + // 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) { + if (OrderStatusCode.ORDER_STATE_SUCCESS == result.getReturnCode()) { + InAppPurchaseData purchaseData = SubscriptionUtils.getInAppPurchaseData(null, + result.getInAppPurchaseData(), result.getInAppDataSignature()); + if (purchaseData != null) { + onPurchaseFinished(purchaseData); + succeed = true; + } else { + logDebug("Purchase failed"); + } + } else if (OrderStatusCode.ORDER_STATE_CANCEL == result.getReturnCode()) { + logDebug("Purchase cancelled"); + } + } 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_PRODUCT_OWNED: + 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..49ff72cd3b --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchasesImpl.java @@ -0,0 +1,184 @@ +package net.osmand.plus.inapp; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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; + liveUpdates = new LiveUpdatesInAppPurchasesFree(); + 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_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 = "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 InAppPurchaseDepthContoursFree extends InAppPurchaseDepthContours { + + private static final String SKU_DEPTH_CONTOURS_FREE = "net.osmand.seadepth"; + + InAppPurchaseDepthContoursFree() { + super(SKU_DEPTH_CONTOURS_FREE); + } + } + + 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 InAppPurchaseLiveUpdatesMonthlyFree extends InAppPurchaseLiveUpdatesMonthly { + + private static final String SKU_LIVE_UPDATES_MONTHLY_HW_FREE = "net.osmand.test.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.test.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.test.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..1dffffc252 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/SubscriptionUtils.java @@ -0,0 +1,139 @@ +/** + * 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/activities/OsmandInAppPurchaseActivity.java b/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java index cb91f7d167..3929307cb9 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,14 @@ 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.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 +28,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 +36,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 +48,36 @@ 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) { + InAppPurchaseHelper purchaseHelper = getMyApplication().getInAppPurchaseHelper(); + if (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 +94,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,7 +119,11 @@ 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); + } } } } @@ -129,8 +151,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 @@ -222,6 +245,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/helpers/DiscountHelper.java b/OsmAnd/src/net/osmand/plus/helpers/DiscountHelper.java index 855f648332..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; @@ -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 c037c13d0a..ff734e6af5 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java @@ -2,6 +2,7 @@ package net.osmand.plus.inapp; import android.annotation.SuppressLint; import android.app.Activity; +import android.content.Intent; import android.os.AsyncTask; import android.text.TextUtils; import android.util.Log; @@ -64,6 +65,7 @@ public abstract class InAppPurchaseHelper { protected InAppPurchaseListener uiActivity = null; public interface InAppPurchaseListener { + void onError(InAppPurchaseTaskType taskType, String error); void onGetItems(); @@ -75,6 +77,13 @@ public abstract class InAppPurchaseHelper { void dismissProgress(InAppPurchaseTaskType taskType); } + public interface InAppPurchaseInitCallback { + + void onSuccess(); + + void onFail(); + } + public enum InAppPurchaseTaskType { REQUEST_INVENTORY, PURCHASE_FULL_VERSION, @@ -82,9 +91,23 @@ public abstract class InAppPurchaseHelper { PURCHASE_DEPTH_CONTOURS } - 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 { @@ -163,9 +186,10 @@ public abstract 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; } @@ -181,7 +205,7 @@ public abstract class InAppPurchaseHelper { return false; } - protected void exec(final @NonNull InAppPurchaseTaskType taskType, final @NonNull InAppRunnable runnable) { + protected void exec(final @NonNull InAppPurchaseTaskType taskType, final @NonNull InAppCommand command) { if (isDeveloperVersion || !Version.isGooglePlayEnabled(ctx)) { notifyDismissProgress(taskType); stop(true); @@ -205,14 +229,20 @@ public abstract class InAppPurchaseHelper { try { processingTask = true; activeTask = taskType; - execImpl(taskType, runnable); + command.resultHandler = new InAppCommandResultHandler() { + @Override + public void onCommandDone(@NonNull InAppCommand command) { + processingTask = false; + } + }; + execImpl(taskType, command); } catch (Exception e) { logError("exec Error", e); stop(true); } } - protected abstract void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppRunnable runnable); + 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())) @@ -224,16 +254,16 @@ public abstract class InAppPurchaseHelper { new RequestInventoryTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } - public abstract void purchaseFullVersion(final Activity activity) throws UnsupportedOperationException; + 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 abstract void purchaseDepthContours(final Activity activity) throws UnsupportedOperationException; + public abstract void purchaseDepthContours(@NonNull final Activity activity) throws UnsupportedOperationException; @SuppressLint("StaticFieldLeak") private class LiveUpdatesPurchaseTask extends AsyncTask { @@ -329,8 +359,8 @@ public abstract class InAppPurchaseHelper { } } - protected abstract InAppRunnable getPurchaseLiveUpdatesCommand(final WeakReference activity, - final String sku, final String payload) throws UnsupportedOperationException; + protected abstract InAppCommand getPurchaseLiveUpdatesCommand(final WeakReference activity, + final String sku, final String payload) throws UnsupportedOperationException; @SuppressLint("StaticFieldLeak") private class RequestInventoryTask extends AsyncTask { @@ -380,7 +410,7 @@ public abstract class InAppPurchaseHelper { } } - protected abstract InAppRunnable getRequestInventoryCommand() throws UnsupportedOperationException; + protected abstract InAppCommand getRequestInventoryCommand() throws UnsupportedOperationException; protected void onSkuDetailsResponseDone(List purchaseInfoList) { final AndroidNetworkUtils.OnRequestResultListener listener = new AndroidNetworkUtils.OnRequestResultListener() { @@ -599,6 +629,10 @@ public abstract class InAppPurchaseHelper { } } + 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); diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java index 8c42448ec7..5ccb49a70e 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,72 +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 static final InAppSubscription[] LIVE_UPDATES_HW_FREE = new InAppSubscription[]{ - new InAppPurchaseLiveUpdatesMonthlyHWFree(), - new InAppPurchaseLiveUpdates3MonthsHWFree(), - new InAppPurchaseLiveUpdatesAnnualHWFree() - }; - - 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.isHuawei(ctx)) { - liveUpdates = new LiveUpdatesInAppPurchasesHWFree(); - } else 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() { @@ -166,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 { @@ -268,27 +192,6 @@ public class InAppPurchases { } } - public static class LiveUpdatesInAppPurchasesFree extends InAppSubscriptionList { - - public LiveUpdatesInAppPurchasesFree() { - super(LIVE_UPDATES_FREE); - } - } - - public static class LiveUpdatesInAppPurchasesHWFree extends InAppSubscriptionList { - - public LiveUpdatesInAppPurchasesHWFree() { - super(LIVE_UPDATES_HW_FREE); - } - } - - public static class LiveUpdatesInAppPurchasesFull extends InAppSubscriptionList { - - public LiveUpdatesInAppPurchasesFull() { - super(LIVE_UPDATES_FULL); - } - } - public abstract static class InAppPurchase { public enum PurchaseState { @@ -310,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; } @@ -792,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); } @@ -818,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); } @@ -848,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); @@ -920,64 +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 class InAppPurchaseLiveUpdatesMonthlyHWFree extends InAppPurchaseLiveUpdatesMonthly { - - private static final String SKU_LIVE_UPDATES_MONTHLY_HW_FREE = "net.osmand.test.monthly"; - - InAppPurchaseLiveUpdatesMonthlyHWFree() { - super(SKU_LIVE_UPDATES_MONTHLY_HW_FREE, 1); - } - - private InAppPurchaseLiveUpdatesMonthlyHWFree(@NonNull String sku) { - super(sku); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyHWFree(sku) : null; - } - } - - public static abstract class InAppPurchaseLiveUpdates3Months extends InAppSubscription { + protected static abstract class InAppPurchaseLiveUpdates3Months extends InAppSubscription { InAppPurchaseLiveUpdates3Months(String skuNoVersion, int version) { super(skuNoVersion, version); @@ -1020,64 +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 class InAppPurchaseLiveUpdates3MonthsHWFree extends InAppPurchaseLiveUpdates3Months { - - private static final String SKU_LIVE_UPDATES_3_MONTHS_HW_FREE = "net.osmand.test.3months"; - - InAppPurchaseLiveUpdates3MonthsHWFree() { - super(SKU_LIVE_UPDATES_3_MONTHS_HW_FREE, 1); - } - - private InAppPurchaseLiveUpdates3MonthsHWFree(@NonNull String sku) { - super(sku); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsHWFree(sku) : null; - } - } - - public static abstract class InAppPurchaseLiveUpdatesAnnual extends InAppSubscription { + protected static abstract class InAppPurchaseLiveUpdatesAnnual extends InAppSubscription { InAppPurchaseLiveUpdatesAnnual(String skuNoVersion, int version) { super(skuNoVersion, version); @@ -1120,63 +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 InAppPurchaseLiveUpdatesAnnualHWFree extends InAppPurchaseLiveUpdatesAnnual { - - private static final String SKU_LIVE_UPDATES_ANNUAL_HW_FREE = "net.osmand.test.annual"; - - InAppPurchaseLiveUpdatesAnnualHWFree() { - super(SKU_LIVE_UPDATES_ANNUAL_HW_FREE, 1); - } - - private InAppPurchaseLiveUpdatesAnnualHWFree(@NonNull String sku) { - super(sku); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualHWFree(sku) : null; - } - } - public static class InAppPurchaseLiveUpdatesOldMonthly extends InAppPurchaseLiveUpdatesMonthly { InAppPurchaseLiveUpdatesOldMonthly(String sku) { @@ -1199,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; - } - } }