diff --git a/OsmAnd/res/layout/osmlive_gone_dialog_fragment.xml b/OsmAnd/res/layout/osmlive_gone_dialog_fragment.xml index a936b85350..1a2a28f1c6 100644 --- a/OsmAnd/res/layout/osmlive_gone_dialog_fragment.xml +++ b/OsmAnd/res/layout/osmlive_gone_dialog_fragment.xml @@ -84,26 +84,11 @@ - - + android:layout_marginTop="@dimen/title_padding"> diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index 086a7470d5..f072d06cb2 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -11,6 +11,11 @@ Thx - Hardy --> + OsmAnd Live subscription is on hold + OsmAnd Live subscription has been paused + OsmAnd Live subscription has been expired + There is a problem with your subscription. Click the button to go to the Google Play subscription settings to fix your payment method. + Manage subscription Start/finish icons Name: A – Z Name: Z – A diff --git a/OsmAnd/src/net/osmand/plus/activities/MapActivity.java b/OsmAnd/src/net/osmand/plus/activities/MapActivity.java index e23e596dcf..c5d93b8c29 100644 --- a/OsmAnd/src/net/osmand/plus/activities/MapActivity.java +++ b/OsmAnd/src/net/osmand/plus/activities/MapActivity.java @@ -84,7 +84,7 @@ import net.osmand.plus.base.BaseOsmAndFragment; import net.osmand.plus.base.ContextMenuFragment; import net.osmand.plus.base.FailSafeFuntions; import net.osmand.plus.base.MapViewTrackingUtilities; -import net.osmand.plus.chooseplan.OsmLiveCancelledDialog; +import net.osmand.plus.chooseplan.OsmLiveGoneDialog; import net.osmand.plus.dashboard.DashboardOnMap; import net.osmand.plus.dialogs.CrashBottomSheetDialogFragment; import net.osmand.plus.dialogs.ImportGpxBottomSheetDialogFragment; @@ -845,8 +845,6 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven getSupportFragmentManager().beginTransaction() .add(R.id.fragmentContainer, new FirstUsageWelcomeFragment(), FirstUsageWelcomeFragment.TAG).commitAllowingStateLoss(); - } else if (!isFirstScreenShowing() && OsmLiveCancelledDialog.shouldShowDialog(app)) { - OsmLiveCancelledDialog.showInstance(getSupportFragmentManager()); } else if (SendAnalyticsBottomSheetDialogFragment.shouldShowDialog(app)) { SendAnalyticsBottomSheetDialogFragment.showInstance(app, getSupportFragmentManager(), null); } @@ -2267,6 +2265,9 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven @Override public void onInAppPurchaseGetItems() { DiscountHelper.checkAndDisplay(this); + if (!isFirstScreenShowing() && OsmLiveGoneDialog.shouldShowDialog(app)) { + OsmLiveGoneDialog.showInstance(app, getSupportFragmentManager()); + } } public enum ShowQuickSearchMode { diff --git a/OsmAnd/src/net/osmand/plus/chooseplan/OsmLiveGoneDialog.java b/OsmAnd/src/net/osmand/plus/chooseplan/OsmLiveGoneDialog.java new file mode 100644 index 0000000000..d402ae93b6 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/chooseplan/OsmLiveGoneDialog.java @@ -0,0 +1,334 @@ +package net.osmand.plus.chooseplan; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; + +import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import net.osmand.PlatformUtil; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.base.BaseOsmAndDialogFragment; +import net.osmand.plus.chooseplan.ChoosePlanDialogFragment.OsmAndFeature; +import net.osmand.plus.inapp.InAppPurchaseHelper; +import net.osmand.plus.inapp.InAppPurchases.InAppSubscription; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.settings.backend.OsmandSettings.OsmandPreference; +import net.osmand.plus.widgets.TextViewEx; +import net.osmand.util.Algorithms; + +import org.apache.commons.logging.Log; + +public abstract class OsmLiveGoneDialog extends BaseOsmAndDialogFragment { + public static final String TAG = OsmLiveGoneDialog.class.getName(); + private static final Log LOG = PlatformUtil.getLog(OsmLiveGoneDialog.class); + + private static final long TIME_BETWEEN_DIALOGS_MSEC = 1000 * 60 * 60 * 24 * 3; // 3 days + + private OsmandApplication app; + private boolean nightMode; + private View osmLiveButton; + + private final OsmAndFeature[] osmLiveFeatures = { + OsmAndFeature.DAILY_MAP_UPDATES, + OsmAndFeature.UNLIMITED_DOWNLOADS, + OsmAndFeature.WIKIPEDIA_OFFLINE, + OsmAndFeature.WIKIVOYAGE_OFFLINE, + OsmAndFeature.CONTOUR_LINES_HILLSHADE_MAPS, + OsmAndFeature.SEA_DEPTH_MAPS, + OsmAndFeature.UNLOCK_ALL_FEATURES, + }; + + public static class OsmLiveOnHoldDialog extends OsmLiveGoneDialog { + public static final String TAG = OsmLiveOnHoldDialog.class.getSimpleName(); + + @Override + protected OsmLiveButtonType getOsmLiveButtonType() { + return OsmLiveButtonType.MANAGE_SUBSCRIPTION; + } + + @Override + protected String getTitle() { + return getString(R.string.subscription_on_hold_title); + } + + @Override + protected String getSubscriptionDescr() { + return getString(R.string.subscription_payment_issue_title); + } + } + + public static class OsmLivePausedDialog extends OsmLiveGoneDialog { + public static final String TAG = OsmLivePausedDialog.class.getSimpleName(); + + @Override + protected OsmLiveButtonType getOsmLiveButtonType() { + return OsmLiveButtonType.MANAGE_SUBSCRIPTION; + } + + @Override + protected String getTitle() { + return getString(R.string.subscription_paused_title); + } + } + + public static class OsmLiveExpiredDialog extends OsmLiveGoneDialog { + public static final String TAG = OsmLiveExpiredDialog.class.getSimpleName(); + + @Override + protected String getTitle() { + return getString(R.string.subscription_expired_title); + } + } + + protected enum OsmLiveButtonType { + PURCHASE_SUBSCRIPTION, + MANAGE_SUBSCRIPTION + } + + protected OsmLiveButtonType getOsmLiveButtonType() { + return OsmLiveButtonType.PURCHASE_SUBSCRIPTION; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + app = getMyApplication(); + nightMode = isNightMode(getMapActivity() != null); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Activity ctx = requireActivity(); + int themeId = nightMode ? R.style.OsmandDarkTheme_DarkActionbar : R.style.OsmandLightTheme_DarkActionbar_LightStatusBar; + Dialog dialog = new Dialog(ctx, themeId); + Window window = dialog.getWindow(); + if (window != null) { + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + if (!getSettings().DO_NOT_USE_ANIMATIONS.get()) { + window.getAttributes().windowAnimations = R.style.Animations_Alpha; + } + if (Build.VERSION.SDK_INT >= 21) { + window.setStatusBarColor(ContextCompat.getColor(ctx, getStatusBarColor())); + } + } + return dialog; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Context ctx = getContext(); + if (ctx == null) { + return null; + } + int themeRes = nightMode ? R.style.OsmandDarkTheme_DarkActionbar : R.style.OsmandLightTheme_DarkActionbar_LightStatusBar; + View view = LayoutInflater.from(new ContextThemeWrapper(getContext(), themeRes)) + .inflate(R.layout.osmlive_gone_dialog_fragment, container, false); + + view.findViewById(R.id.button_close).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + TextViewEx title = (TextViewEx) view.findViewById(R.id.title); + title.setText(getTitle()); + TextViewEx infoDescr = (TextViewEx) view.findViewById(R.id.info_description); + StringBuilder descr = new StringBuilder(); + String subscriptionDescr = getSubscriptionDescr(); + if (!Algorithms.isEmpty(subscriptionDescr)) { + descr.append(subscriptionDescr).append("\n\n"); + } + descr.append(getString(R.string.purchase_cancelled_dialog_descr)); + for (OsmAndFeature feature : osmLiveFeatures) { + descr.append("\n").append("— ").append(feature.toHumanString(ctx)); + } + infoDescr.setText(descr); + + osmLiveButton = view.findViewById(R.id.card_button); + + return view; + } + + protected abstract String getTitle(); + + protected String getSubscriptionDescr() { + return null; + } + + @Nullable + public MapActivity getMapActivity() { + Activity activity = getActivity(); + if (activity instanceof MapActivity) { + return (MapActivity) activity; + } + return null; + } + + @Override + public void onResume() { + super.onResume(); + + MapActivity mapActivity = getMapActivity(); + if (mapActivity != null) { + mapActivity.disableDrawer(); + } + + setupOsmLiveButton(); + + OsmandPreference firstTimeShownTime = app.getSettings().LIVE_UPDATES_EXPIRED_FIRST_DLG_SHOWN_TIME; + OsmandPreference secondTimeShownTime = app.getSettings().LIVE_UPDATES_EXPIRED_SECOND_DLG_SHOWN_TIME; + if (firstTimeShownTime.get() == 0) { + firstTimeShownTime.set(System.currentTimeMillis()); + } else if (secondTimeShownTime.get() == 0) { + secondTimeShownTime.set(System.currentTimeMillis()); + } + } + + @Override + public void onPause() { + super.onPause(); + + MapActivity mapActivity = getMapActivity(); + if (mapActivity != null) { + mapActivity.enableDrawer(); + } + } + + @ColorRes + protected int getStatusBarColor() { + return nightMode ? R.color.status_bar_wikivoyage_dark : R.color.status_bar_wikivoyage_light; + } + + private void setupOsmLiveButton() { + if (osmLiveButton != null) { + TextViewEx buttonTitle = (TextViewEx) osmLiveButton.findViewById(R.id.card_button_title); + TextViewEx buttonSubtitle = (TextViewEx) osmLiveButton.findViewById(R.id.card_button_subtitle); + switch (getOsmLiveButtonType()) { + case PURCHASE_SUBSCRIPTION: + buttonTitle.setText(getString(R.string.osm_live_plan_pricing)); + osmLiveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + FragmentActivity activity = getActivity(); + if (activity != null) { + ChoosePlanDialogFragment.showOsmLiveInstance(activity.getSupportFragmentManager()); + } + } + }); + break; + case MANAGE_SUBSCRIPTION: + buttonTitle.setText(getString(R.string.manage_subscription)); + osmLiveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + FragmentActivity activity = getActivity(); + if (activity != null) { + InAppSubscription expiredSubscription = getExpiredSubscription((OsmandApplication) activity.getApplication()); + if (expiredSubscription != null) { + manageSubscription(expiredSubscription.getSku()); + } + } + } + }); + break; + } + buttonSubtitle.setVisibility(View.GONE); + buttonTitle.setVisibility(View.VISIBLE); + osmLiveButton.findViewById(R.id.card_button_progress).setVisibility(View.GONE); + } + } + + private void manageSubscription(@Nullable String sku) { + Context ctx = getContext(); + if (ctx != null) { + String url = "https://play.google.com/store/account/subscriptions?package=" + ctx.getPackageName(); + if (!Algorithms.isEmpty(sku)) { + url += "&sku=" + sku; + } + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(intent); + } + } + + @Nullable + private static InAppSubscription getExpiredSubscription(@NonNull OsmandApplication app) { + if (!app.getSettings().LIVE_UPDATES_PURCHASED.get()) { + InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper(); + return purchaseHelper.getLiveUpdates().getTopExpiredSubscription(); + } + return null; + } + + public static boolean shouldShowDialog(@NonNull OsmandApplication app) { + InAppSubscription expiredSubscription = getExpiredSubscription(app); + if (expiredSubscription == null) { + return false; + } + OsmandSettings settings = app.getSettings(); + long firstTimeShownTime = settings.LIVE_UPDATES_EXPIRED_FIRST_DLG_SHOWN_TIME.get(); + long secondTimeShownTime = settings.LIVE_UPDATES_EXPIRED_SECOND_DLG_SHOWN_TIME.get(); + return firstTimeShownTime == 0 + || (System.currentTimeMillis() - firstTimeShownTime > TIME_BETWEEN_DIALOGS_MSEC && secondTimeShownTime == 0); + } + + public static void showInstance(@NonNull OsmandApplication app, @NonNull FragmentManager fm) { + try { + InAppSubscription expiredSubscription = getExpiredSubscription(app); + if (expiredSubscription == null) { + return; + } + String tag = null; + DialogFragment fragment = null; + switch (expiredSubscription.getState()) { + case ON_HOLD: + tag = OsmLiveOnHoldDialog.TAG; + if (fm.findFragmentByTag(tag) == null) { + fragment = new OsmLiveOnHoldDialog(); + } + break; + case PAUSED: + tag = OsmLivePausedDialog.TAG; + if (fm.findFragmentByTag(tag) == null) { + fragment = new OsmLivePausedDialog(); + } + break; + case EXPIRED: + tag = OsmLiveExpiredDialog.TAG; + if (fm.findFragmentByTag(tag) == null) { + fragment = new OsmLiveExpiredDialog(); + } + break; + } + if (fragment != null) { + fragment.show(fm, tag); + } + } catch (RuntimeException e) { + LOG.error("showInstance", e); + } + } +} diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java index b42b57f045..e9552bd9eb 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java @@ -27,6 +27,8 @@ import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.Currency; import java.util.List; import java.util.Locale; @@ -258,6 +260,28 @@ public class InAppPurchases { } return null; } + + @Nullable + public InAppSubscription getTopExpiredSubscription() { + List expiredSubscriptions = new ArrayList<>(); + for (InAppSubscription s : getAllSubscriptions()) { + if (s.getState().isGone()) { + expiredSubscriptions.add(s); + } + } + Collections.sort(expiredSubscriptions, new Comparator() { + @Override + public int compare(InAppSubscription s1, InAppSubscription s2) { + int orderS1 = s1.getState().ordinal(); + int orderS2 = s2.getState().ordinal(); + if (orderS1 != orderS2) { + return (orderS1 < orderS2) ? -1 : ((orderS1 == orderS2) ? 0 : 1); + } + return Double.compare(s1.getMonthlyPriceValue(), s2.getMonthlyPriceValue()); + } + }); + return expiredSubscriptions.isEmpty() ? null : expiredSubscriptions.get(0); + } } public static class LiveUpdatesInAppPurchasesFree extends InAppSubscriptionList { @@ -636,9 +660,45 @@ public class InAppPurchases { private String subscriptionPeriodString; private Period subscriptionPeriod; private boolean upgrade = false; + private SubscriptionState state = SubscriptionState.UNDEFINED; + private SubscriptionState prevState = SubscriptionState.UNDEFINED; private InAppSubscriptionIntroductoryInfo introductoryInfo; + public enum SubscriptionState { + UNDEFINED("undefined"), + ACTIVE("active"), + CANCELLED("cancelled"), + IN_GRACE_PERIOD("in_grace_period"), + ON_HOLD("on_hold"), + PAUSED("paused"), + EXPIRED("expired"); + + private final String stateStr; + + SubscriptionState(@NonNull String stateStr) { + this.stateStr = stateStr; + } + + public String getStateStr() { + return stateStr; + } + + @NonNull + public static SubscriptionState getByStateStr(@NonNull String stateStr) { + for (SubscriptionState state : SubscriptionState.values()) { + if (state.stateStr.equals(stateStr)) { + return state; + } + } + return UNDEFINED; + } + + public boolean isGone() { + return this == ON_HOLD || this == PAUSED || this == EXPIRED; + } + } + InAppSubscription(@NonNull String skuNoVersion, int version) { super(skuNoVersion + "_v" + version); this.skuNoVersion = skuNoVersion; @@ -674,6 +734,28 @@ public class InAppPurchases { return upgrade; } + @NonNull + public SubscriptionState getState() { + return state; + } + + public void setState(@NonNull SubscriptionState state) { + this.state = state; + } + + @NonNull + public SubscriptionState getPrevState() { + return prevState; + } + + public void setPrevState(@NonNull SubscriptionState prevState) { + this.prevState = prevState; + } + + public boolean hasStateChanged() { + return state != prevState; + } + public boolean isAnyPurchased() { if (isPurchased()) { return true; diff --git a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java index efa7147e6c..a0295bd895 100644 --- a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java +++ b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java @@ -2003,9 +2003,8 @@ public class OsmandSettings { public final OsmandPreference BILLING_PURCHASE_TOKEN_SENT = new BooleanPreference("billing_purchase_token_sent", false).makeGlobal(); public final OsmandPreference BILLING_PURCHASE_TOKENS_SENT = new StringPreference("billing_purchase_tokens_sent", "").makeGlobal(); public final OsmandPreference LIVE_UPDATES_PURCHASED = new BooleanPreference("billing_live_updates_purchased", false).makeGlobal(); - public final OsmandPreference LIVE_UPDATES_PURCHASE_CANCELLED_TIME = new LongPreference("live_updates_purchase_cancelled_time", 0).makeGlobal(); - public final OsmandPreference LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN = new BooleanPreference("live_updates_purchase_cancelled_first_dlg_shown", false).makeGlobal(); - public final OsmandPreference LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN = new BooleanPreference("live_updates_purchase_cancelled_second_dlg_shown", false).makeGlobal(); + public final OsmandPreference LIVE_UPDATES_EXPIRED_FIRST_DLG_SHOWN_TIME = new LongPreference("live_updates_expired_first_dlg_shown_time", 0).makeGlobal(); + public final OsmandPreference LIVE_UPDATES_EXPIRED_SECOND_DLG_SHOWN_TIME = new LongPreference("live_updates_expired_second_dlg_shown_time", 0).makeGlobal(); public final OsmandPreference FULL_VERSION_PURCHASED = new BooleanPreference("billing_full_version_purchased", false).makeGlobal(); public final OsmandPreference DEPTH_CONTOURS_PURCHASED = new BooleanPreference("billing_sea_depth_purchased", false).makeGlobal(); public final OsmandPreference EMAIL_SUBSCRIBED = new BooleanPreference("email_subscribed", false).makeGlobal();