diff --git a/OsmAnd-java/src/main/java/net/osmand/Period.java b/OsmAnd-java/src/main/java/net/osmand/Period.java new file mode 100644 index 0000000000..d6bd911257 --- /dev/null +++ b/OsmAnd-java/src/main/java/net/osmand/Period.java @@ -0,0 +1,171 @@ +package net.osmand; + +import java.text.ParseException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Period { + + public enum PeriodUnit { + YEAR("Y"), + MONTH("M"), + WEEK("W"), + DAY("D"); + + private String unitStr; + + PeriodUnit(String unitStr) { + this.unitStr = unitStr; + } + + public String getUnitStr() { + return unitStr; + } + + public double getMonthsValue() { + switch (this) { + case YEAR: + return 12d; + case MONTH: + return 1d; + case WEEK: + return 1/4d; + case DAY: + return 1/30d; + } + return 0d; + } + + public static PeriodUnit parseUnit(String unitStr) { + for (PeriodUnit unit : values()) { + if (unit.unitStr.equals(unitStr)) { + return unit; + } + } + return null; + } + } + + private static final Pattern PATTERN = + Pattern.compile("^P(?:([-+]?[0-9]+)([YMWD]))?$", Pattern.CASE_INSENSITIVE); + + private PeriodUnit unit; + + private final int numberOfUnits; + + public static Period ofYears(int years) { + return new Period(PeriodUnit.YEAR, years); + } + + public static Period ofMonths(int months) { + return new Period(PeriodUnit.MONTH, months); + } + + public static Period ofWeeks(int weeks) { + return new Period(PeriodUnit.WEEK, weeks); + } + + public static Period ofDays(int days) { + return new Period(PeriodUnit.DAY, days); + } + + public PeriodUnit getUnit() { + return unit; + } + + public int getNumberOfUnits() { + return numberOfUnits; + } + + /** + * Obtains a {@code Period} from a text string such as {@code PnY PnM PnD PnW}. + *

+ * This will parse the string produced by {@code toString()} which is + * based on the ISO-8601 period formats {@code PnY PnM PnD PnW}. + *

+ * The string cannot start with negative sign. + * The ASCII letter "P" is next in upper or lower case. + *

+ * For example, the following are valid inputs: + *

+	 *   "P2Y"             -- Period.ofYears(2)
+	 *   "P3M"             -- Period.ofMonths(3)
+	 *   "P4W"             -- Period.ofWeeks(4)
+	 *   "P5D"             -- Period.ofDays(5)
+	 * 
+ * + * @param text the text to parse, not null + * @return the parsed period, not null + * @throws ParseException if the text cannot be parsed to a period + */ + public static Period parse(CharSequence text) throws ParseException { + Matcher matcher = PATTERN.matcher(text); + if (matcher.matches()) { + String numberOfUnitsMatch = matcher.group(1); + String unitMatch = matcher.group(2); + if (numberOfUnitsMatch != null && unitMatch != null) { + try { + int numberOfUnits = parseNumber(numberOfUnitsMatch); + PeriodUnit unit = PeriodUnit.parseUnit(unitMatch); + return new Period(unit, numberOfUnits); + } catch (IllegalArgumentException ex) { + throw new ParseException("Text cannot be parsed to a Period: " + text, 0); + } + } + } + throw new ParseException("Text cannot be parsed to a Period: " + text, 0); + } + + private static int parseNumber(String str) throws ParseException { + if (str == null) { + return 0; + } + return Integer.parseInt(str); + } + + public Period(PeriodUnit unit, int numberOfUnits) { + if (unit == null) { + throw new IllegalArgumentException("PeriodUnit cannot be null"); + } + this.unit = unit; + this.numberOfUnits = numberOfUnits; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof Period) { + Period other = (Period) obj; + return unit.ordinal() == other.unit.ordinal() && numberOfUnits == other.numberOfUnits; + } + return false; + } + + @Override + public int hashCode() { + return unit.ordinal() + Integer.rotateLeft(numberOfUnits, 8); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append('P').append(numberOfUnits); + switch (unit) { + case YEAR: + buf.append('Y'); + break; + case MONTH: + buf.append('M'); + break; + case WEEK: + buf.append('W'); + break; + case DAY: + buf.append('D'); + break; + } + return buf.toString(); + } +} diff --git a/OsmAnd/res/layout/purchase_dialog_card_button_active_ex.xml b/OsmAnd/res/layout/purchase_dialog_card_button_active_ex.xml index 5647137c1f..98db3af15f 100644 --- a/OsmAnd/res/layout/purchase_dialog_card_button_active_ex.xml +++ b/OsmAnd/res/layout/purchase_dialog_card_button_active_ex.xml @@ -6,52 +6,51 @@ android:layout_height="wrap_content" android:layout_marginLeft="1dp" android:layout_marginRight="1dp" - android:background="?attr/subscription_active_bg_color" android:orientation="vertical"> - - - - + android:orientation="horizontal"> + android:layout_weight="1" + android:orientation="vertical"> + + + + + + @@ -76,7 +84,7 @@ @@ -100,7 +108,7 @@ @@ -108,7 +116,8 @@ @@ -121,7 +130,7 @@ android:letterSpacing="@dimen/text_button_letter_spacing" android:maxWidth="@dimen/dialog_button_ex_max_width" android:minWidth="@dimen/dialog_button_ex_min_width" - android:text="@string/shared_string_cancel" + android:text="@string/cancel_subscription" android:textColor="@color/color_white" android:textSize="@dimen/text_button_text_size" osmand:typeface="@string/font_roboto_medium" /> @@ -136,16 +145,8 @@ android:id="@+id/div" android:layout_width="match_parent" android:layout_height="1dp" - android:layout_marginLeft="@dimen/card_padding" - android:layout_marginRight="@dimen/card_padding" - android:background="?attr/subscription_active_div_color" - android:visibility="gone" /> - - diff --git a/OsmAnd/res/layout/purchase_dialog_card_button_ex.xml b/OsmAnd/res/layout/purchase_dialog_card_button_ex.xml index 2dba96adf7..05465c67db 100644 --- a/OsmAnd/res/layout/purchase_dialog_card_button_ex.xml +++ b/OsmAnd/res/layout/purchase_dialog_card_button_ex.xml @@ -12,82 +12,52 @@ android:id="@+id/button_container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?attr/selectableItemBackground" android:baselineAligned="false" - android:minHeight="@dimen/dialog_button_ex_height" - android:orientation="horizontal" + android:orientation="vertical" android:paddingLeft="@dimen/card_padding" android:paddingRight="@dimen/card_padding"> - + android:text="@string/osm_live_payment_contribute_descr" + android:textColor="?attr/card_description_text_color" + android:textSize="@dimen/default_desc_text_size" + android:visibility="gone" + osmand:typeface="@string/font_roboto_regular" + tools:visibility="visible" /> - - - - - - - + @@ -95,7 +65,7 @@ + tools:text="7,99€ / year" /> @@ -127,7 +97,7 @@ @@ -135,7 +105,7 @@ + tools:text="$3.99 for six month\nthan $7.49 / year" /> @@ -164,8 +134,7 @@ android:id="@+id/div" android:layout_width="match_parent" android:layout_height="1dp" - android:layout_marginLeft="@dimen/card_padding" - android:layout_marginRight="@dimen/card_padding" + android:layout_marginTop="@dimen/content_padding_small" android:background="?attr/wikivoyage_card_divider_color" android:visibility="gone" /> diff --git a/OsmAnd/res/layout/purchase_dialog_osm_live_card.xml b/OsmAnd/res/layout/purchase_dialog_osm_live_card.xml index 5697540951..03612b69cc 100644 --- a/OsmAnd/res/layout/purchase_dialog_osm_live_card.xml +++ b/OsmAnd/res/layout/purchase_dialog_osm_live_card.xml @@ -64,6 +64,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="2dp" + android:paddingBottom="@dimen/content_padding_small" android:orientation="vertical" android:visibility="gone" /> diff --git a/OsmAnd/res/values-ru/strings.xml b/OsmAnd/res/values-ru/strings.xml index 4868711e7d..a795691cac 100644 --- a/OsmAnd/res/values-ru/strings.xml +++ b/OsmAnd/res/values-ru/strings.xml @@ -1,5 +1,26 @@ + День + Дня + Дней + Неделя + Недели + Недель + Месяц + Месяца + Месяцев + Год + Года + Лет + Три месяца + Бесплатно + Получите %1$d %2$s со скидкой %3$s. + Начать бесплатный период в %1$d %2$s. + %1$s за первый %2$s + %1$s за первые %2$s + затем %1$s + Отменить подписку + %1$s • Экономия %2$s Автозимник Ледовый автозимник Зимники diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index d00e230512..57674c0e0a 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -11,6 +11,27 @@ Thx - Hardy --> + Day + Days + Days + Week + Weeks + Weeks + Month + Months + Months + Year + Years + Years + Three months + Free + Get %1$d %2$s at %3$s off. + Start your %1$d %2$s free trial. + %1$s for first %2$s + %1$s for first %2$s + then %1$s + Cancel subscription + %1$s • Save %2$s Wagon Pickup truck Default diff --git a/OsmAnd/src/net/osmand/AndroidUtils.java b/OsmAnd/src/net/osmand/AndroidUtils.java index 4957d02d21..a7e923fde5 100644 --- a/OsmAnd/src/net/osmand/AndroidUtils.java +++ b/OsmAnd/src/net/osmand/AndroidUtils.java @@ -30,6 +30,8 @@ import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; +import android.support.v4.text.TextUtilsCompat; +import android.support.v4.view.ViewCompat; import android.text.ParcelableSpan; import android.text.Spannable; import android.text.SpannableString; @@ -62,6 +64,7 @@ import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import static android.content.Context.POWER_SERVICE; @@ -598,4 +601,8 @@ public class AndroidUtils { return baseString; } } + + public static boolean isRTL() { + return TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL; + } } diff --git a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java index 620df4a241..144748dcd8 100644 --- a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java +++ b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java @@ -45,6 +45,7 @@ import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseListener; import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseTaskType; 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.liveupdates.SubscriptionFragment; import net.osmand.plus.srtmplugin.SRTMPlugin; import net.osmand.plus.widgets.TextViewEx; @@ -341,38 +342,40 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment } else if (osmLiveCardButtonsContainer != null) { osmLiveCardButtonsContainer.removeAllViews(); View lastBtn = null; - InAppSubscription monthlyLiveUpdates = purchaseHelper.getMonthlyLiveUpdates(); - double regularMonthlyPrice = monthlyLiveUpdates.getPriceValue(); List visibleSubscriptions = purchaseHelper.getLiveUpdates().getVisibleSubscriptions(); - boolean anyPurchased = false; + boolean anyPurchasedOrIntroducted = false; for (final InAppSubscription s : visibleSubscriptions) { - if (s.isPurchased()) { - anyPurchased = true; + if (s.isPurchased() || s.getIntroductoryInfo() != null) { + anyPurchasedOrIntroducted = true; break; } } for (final InAppSubscription s : visibleSubscriptions) { + InAppSubscriptionIntroductoryInfo introductoryInfo = s.getIntroductoryInfo(); + boolean hasIntroductoryInfo = introductoryInfo != null; + CharSequence priceTitle = hasIntroductoryInfo ? introductoryInfo.getFormattedDescription(ctx) : s.getPrice(ctx); + CharSequence descriptionText = hasIntroductoryInfo ? + introductoryInfo.getDescriptionTitle(ctx) : s.getDescription(ctx, purchaseHelper.getMonthlyLiveUpdates()); if (s.isPurchased()) { View buttonPurchased = inflate(R.layout.purchase_dialog_card_button_active_ex, osmLiveCardButtonsContainer); - View buttonContainer = buttonPurchased.findViewById(R.id.button_container); TextViewEx title = (TextViewEx) buttonPurchased.findViewById(R.id.title); TextViewEx description = (TextViewEx) buttonPurchased.findViewById(R.id.description); + TextViewEx descriptionContribute = (TextViewEx) buttonPurchased.findViewById(R.id.description_contribute); + descriptionContribute.setVisibility(s.isDonationSupported() ? View.VISIBLE : View.GONE); TextViewEx buttonTitle = (TextViewEx) buttonPurchased.findViewById(R.id.button_title); View buttonView = buttonPurchased.findViewById(R.id.button_view); View buttonCancelView = buttonPurchased.findViewById(R.id.button_cancel_view); - View divTop = buttonPurchased.findViewById(R.id.div_top); - View divBottom = buttonPurchased.findViewById(R.id.div_bottom); View div = buttonPurchased.findViewById(R.id.div); + AppCompatImageView rightImage = buttonPurchased.findViewById(R.id.right_image); title.setText(s.getTitle(ctx)); - description.setText(s.getDescription(ctx)); - buttonTitle.setText(s.getPrice(ctx)); + description.setText(descriptionText); + buttonTitle.setText(priceTitle); buttonView.setVisibility(View.VISIBLE); buttonCancelView.setVisibility(View.GONE); buttonPurchased.setOnClickListener(null); - divTop.setVisibility(View.VISIBLE); - div.setVisibility(View.VISIBLE); - divBottom.setVisibility(View.GONE); + div.setVisibility(View.GONE); + rightImage.setVisibility(View.GONE); if (s.isDonationSupported()) { buttonPurchased.setOnClickListener(new OnClickListener() { @Override @@ -387,14 +390,12 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment osmLiveCardButtonsContainer.addView(buttonPurchased); View buttonCancel = inflate(R.layout.purchase_dialog_card_button_active_ex, osmLiveCardButtonsContainer); - buttonContainer = buttonCancel.findViewById(R.id.button_container); title = (TextViewEx) buttonCancel.findViewById(R.id.title); description = (TextViewEx) buttonCancel.findViewById(R.id.description); buttonView = buttonCancel.findViewById(R.id.button_view); buttonCancelView = buttonCancel.findViewById(R.id.button_cancel_view); - divTop = buttonCancel.findViewById(R.id.div_top); - divBottom = buttonCancel.findViewById(R.id.div_bottom); div = buttonCancel.findViewById(R.id.div); + rightImage = buttonPurchased.findViewById(R.id.right_image); title.setText(getString(R.string.osm_live_payment_current_subscription)); description.setText(s.getRenewDescription(ctx)); @@ -406,70 +407,34 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment manageSubscription(ctx, s.getSku()); } }); - divTop.setVisibility(View.GONE); - div.setVisibility(View.GONE); - divBottom.setVisibility(View.VISIBLE); + div.setVisibility(View.VISIBLE); + rightImage.setVisibility(View.VISIBLE); osmLiveCardButtonsContainer.addView(buttonCancel); - - if (lastBtn != null) { - View lastBtnDiv = lastBtn.findViewById(R.id.div); - if (lastBtnDiv != null) { - lastBtnDiv.setVisibility(View.GONE); - } - View lastBtnDivBottom = lastBtn.findViewById(R.id.div_bottom); - if (lastBtnDivBottom != null) { - lastBtnDivBottom.setVisibility(View.GONE); - } - } lastBtn = buttonCancel; } else { View button = inflate(R.layout.purchase_dialog_card_button_ex, osmLiveCardButtonsContainer); TextViewEx title = (TextViewEx) button.findViewById(R.id.title); TextViewEx description = (TextViewEx) button.findViewById(R.id.description); + TextViewEx descriptionContribute = (TextViewEx) button.findViewById(R.id.description_contribute); + descriptionContribute.setVisibility(s.isDonationSupported() ? View.VISIBLE : View.GONE); View buttonView = button.findViewById(R.id.button_view); View buttonExView = button.findViewById(R.id.button_ex_view); TextViewEx buttonTitle = (TextViewEx) button.findViewById(R.id.button_title); TextViewEx buttonExTitle = (TextViewEx) button.findViewById(R.id.button_ex_title); - buttonView.setVisibility(anyPurchased ? View.VISIBLE : View.GONE); - buttonExView.setVisibility(!anyPurchased ? View.VISIBLE : View.GONE); + boolean showSolidButton = !anyPurchasedOrIntroducted || hasIntroductoryInfo; + buttonView.setVisibility(!showSolidButton ? View.VISIBLE : View.GONE); + buttonExView.setVisibility(showSolidButton ? View.VISIBLE : View.GONE); - TextViewEx discountRegular = (TextViewEx) button.findViewById(R.id.discount_banner_regular); - TextViewEx discountActive = (TextViewEx) button.findViewById(R.id.discount_banner_active); View div = button.findViewById(R.id.div); title.setText(s.getTitle(ctx)); - description.setText(s.getDescription(ctx)); - buttonTitle.setText(s.getPrice(ctx)); - buttonExTitle.setText(s.getPrice(ctx)); + description.setText(descriptionText); + buttonTitle.setText(priceTitle); + buttonExTitle.setText(priceTitle); - if (regularMonthlyPrice > 0 && s.getMonthlyPriceValue() > 0 && s.getMonthlyPriceValue() < regularMonthlyPrice) { - int discount = (int) ((1 - s.getMonthlyPriceValue() / regularMonthlyPrice) * 100d); - String discountStr = discount + "%"; - if (discount > 50) { - discountActive.setText(String.format(" %s ", getString(R.string.osm_live_payment_discount_descr, discountStr))); - discountActive.setVisibility(View.VISIBLE); - discountRegular.setVisibility(View.GONE); - } else if (discount > 0) { - discountActive.setVisibility(View.GONE); - discountRegular.setText(String.format(" %s ", getString(R.string.osm_live_payment_discount_descr, discountStr))); - discountRegular.setVisibility(View.VISIBLE); - } else { - discountActive.setVisibility(View.GONE); - discountRegular.setVisibility(View.GONE); - } - } else { - discountActive.setVisibility(View.GONE); - discountRegular.setVisibility(View.GONE); - } - button.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - subscribe(s.getSku()); - } - }); - if (anyPurchased) { + if (!showSolidButton) { buttonView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -494,10 +459,6 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment if (div != null) { div.setVisibility(View.GONE); } - View divBottom = lastBtn.findViewById(R.id.div_bottom); - if (divBottom != null) { - divBottom.setVisibility(View.GONE); - } } if (osmLiveCardProgress != null) { osmLiveCardProgress.setVisibility(View.GONE); diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java index a1c04b63bc..9b4b26afd0 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java @@ -27,6 +27,7 @@ import net.osmand.plus.inapp.InAppPurchases.InAppPurchase; import net.osmand.plus.inapp.InAppPurchases.InAppPurchase.PurchaseState; import net.osmand.plus.inapp.InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription; import net.osmand.plus.inapp.InAppPurchases.InAppSubscription; +import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionIntroductoryInfo; import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionList; import net.osmand.plus.inapp.util.BillingManager; import net.osmand.plus.inapp.util.BillingManager.BillingUpdatesListener; @@ -39,6 +40,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.lang.ref.WeakReference; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -603,7 +605,26 @@ public class InAppPurchaseHelper { String subscriptionPeriod = skuDetails.getSubscriptionPeriod(); if (!Algorithms.isEmpty(subscriptionPeriod)) { if (inAppPurchase instanceof InAppSubscription) { - ((InAppSubscription) inAppPurchase).setSubscriptionPeriod(subscriptionPeriod); + try { + ((InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod); + } catch (ParseException e) { + LOG.error(e); + } + } + } + if (inAppPurchase instanceof InAppSubscription) { + String introductoryPrice = skuDetails.getIntroductoryPrice(); + String introductoryPricePeriod = skuDetails.getIntroductoryPricePeriod(); + String introductoryPriceCycles = skuDetails.getIntroductoryPriceCycles(); + long introductoryPriceAmountMicros = skuDetails.getIntroductoryPriceAmountMicros(); + if (!Algorithms.isEmpty(introductoryPrice)) { + InAppSubscription s = (InAppSubscription) inAppPurchase; + try { + s.setIntroductoryInfo(new InAppSubscriptionIntroductoryInfo(s, introductoryPrice, + introductoryPriceAmountMicros, introductoryPricePeriod, introductoryPriceCycles)); + } catch (ParseException e) { + LOG.error(e); + } } } } diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java index a321d56eea..90b73064d7 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java @@ -1,20 +1,28 @@ package net.osmand.plus.inapp; import android.content.Context; +import android.graphics.Typeface; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; import android.text.Spannable; import android.text.SpannableStringBuilder; -import android.text.style.StyleSpan; +import android.text.style.ForegroundColorSpan; 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; import java.text.NumberFormat; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Currency; @@ -169,7 +177,7 @@ public class InAppPurchases { private List subscriptions; InAppSubscriptionList(@NonNull InAppSubscription[] subscriptionsArray) { - this.subscriptions = Arrays.asList(subscriptionsArray);; + this.subscriptions = Arrays.asList(subscriptionsArray); } private List getSubscriptions() { @@ -427,13 +435,204 @@ public class InAppPurchases { } } + public static class InAppSubscriptionIntroductoryInfo { + + private InAppSubscription subscription; + + private String introductoryPrice; + private long introductoryPriceAmountMicros; + private String introductoryPeriodString; + private int introductoryCycles; + + private double introductoryPriceValue; + private Period introductoryPeriod; + + public InAppSubscriptionIntroductoryInfo(@NonNull InAppSubscription subscription, + String introductoryPrice, + long introductoryPriceAmountMicros, + String introductoryPeriodString, + String introductoryCycles) throws ParseException { + this.subscription = subscription; + this.introductoryPrice = introductoryPrice; + this.introductoryPriceAmountMicros = introductoryPriceAmountMicros; + this.introductoryPeriodString = introductoryPeriodString; + try { + this.introductoryCycles = Integer.parseInt(introductoryCycles); + } catch (NumberFormatException e) { + throw new ParseException("Cannot parse introductoryCycles = " + introductoryCycles, 0); + } + introductoryPriceValue = introductoryPriceAmountMicros / 1000000d; + introductoryPeriod = Period.parse(introductoryPeriodString); + } + + public String getIntroductoryPrice() { + return introductoryPrice; + } + + public long getIntroductoryPriceAmountMicros() { + return introductoryPriceAmountMicros; + } + + public String getIntroductoryPeriodString() { + return introductoryPeriodString; + } + + public int getIntroductoryCycles() { + return introductoryCycles; + } + + public double getIntroductoryPriceValue() { + return introductoryPriceValue; + } + + public double getIntroductoryMonthlyPriceValue() { + return introductoryPriceValue / + (introductoryPeriod.getUnit().getMonthsValue() * introductoryPeriod.getNumberOfUnits()); + } + + public Period getIntroductoryPeriod() { + return introductoryPeriod; + } + + public long getTotalPeriods() { + return introductoryPeriod.getNumberOfUnits() * introductoryCycles; + } + + private String getTotalUnitsString(@NonNull Context ctx, boolean original) { + String unitStr = ""; + Period subscriptionPeriod = subscription.getSubscriptionPeriod(); + PeriodUnit unit = original && subscriptionPeriod != null ? subscriptionPeriod.getUnit() : introductoryPeriod.getUnit(); + long totalPeriods = original && subscriptionPeriod != null ? subscriptionPeriod.getNumberOfUnits() : getTotalPeriods(); + switch (unit) { + case YEAR: + unitStr = ctx.getString(R.string.year); + break; + case MONTH: + if (totalPeriods == 1) { + unitStr = ctx.getString(R.string.month); + } else if (totalPeriods < 5) { + unitStr = ctx.getString(R.string.months_2_4); + } else { + unitStr = ctx.getString(R.string.months_5); + } + break; + case WEEK: + if (totalPeriods == 1) { + unitStr = ctx.getString(R.string.week); + } else if (totalPeriods < 5) { + unitStr = ctx.getString(R.string.weeks_2_4); + } else { + unitStr = ctx.getString(R.string.weeks_5); + } + break; + case DAY: + if (totalPeriods == 1) { + unitStr = ctx.getString(R.string.day); + } else if (totalPeriods < 5) { + unitStr = ctx.getString(R.string.days_2_4); + } else { + unitStr = ctx.getString(R.string.days_5); + } + break; + } + return unitStr; + } + + private String getUnitString(@NonNull Context ctx) { + PeriodUnit unit = introductoryPeriod.getUnit(); + switch (unit) { + case YEAR: + return ctx.getString(R.string.year); + case MONTH: + return ctx.getString(R.string.month); + case WEEK: + return ctx.getString(R.string.week); + case DAY: + return ctx.getString(R.string.day); + } + return ""; + } + + private String getDisountPeriodString(String unitStr, long totalPeriods) { + if (totalPeriods == 1) + return unitStr; + if (AndroidUtils.isRTL()) { + return unitStr + " " + totalPeriods; + } else { + return totalPeriods + " " + unitStr; + } + } + + public CharSequence getDescriptionTitle(@NonNull Context ctx) { + long totalPeriods = getTotalPeriods(); + String unitStr = getTotalUnitsString(ctx, false).toLowerCase(); + int discountPercent = subscription.getDiscountPercent(null); + return ctx.getString(R.string.get_discount_title, totalPeriods, unitStr, discountPercent + "%"); + } + + public CharSequence getFormattedDescription(@NonNull Context ctx) { + long totalPeriods = getTotalPeriods(); + String singleUnitStr = getUnitString(ctx).toLowerCase(); + String unitStr = getTotalUnitsString(ctx, false).toLowerCase(); + long numberOfUnits = introductoryPeriod.getNumberOfUnits(); + Period subscriptionPeriod = subscription.getSubscriptionPeriod(); + long originalNumberOfUnits = subscriptionPeriod != null ? subscriptionPeriod.getNumberOfUnits() : 1; + String originalUnitsStr = getTotalUnitsString(ctx, true).toLowerCase(); + String originalPriceStr = subscription.getPrice(ctx); + String priceStr = introductoryPrice; + + String pricePeriod; + String originalPricePeriod; + + if (AndroidUtils.isRTL()) { + pricePeriod = singleUnitStr + " / " + priceStr; + originalPricePeriod = originalUnitsStr + " / " + originalPriceStr; + if (numberOfUnits > 1) { + pricePeriod = unitStr + " " + numberOfUnits + " / " + priceStr; + } + if (originalNumberOfUnits == 3 && subscriptionPeriod.getUnit() == PeriodUnit.MONTH) { + originalPricePeriod = ctx.getString(R.string.months_3).toLowerCase() + " / " + originalPriceStr; + } else if (originalNumberOfUnits > 1) { + originalPricePeriod = originalUnitsStr + " " + originalNumberOfUnits + " / " + originalPriceStr; + } + } else { + pricePeriod = priceStr + " / " + singleUnitStr; + originalPricePeriod = originalPriceStr + " / " + originalUnitsStr; + if (numberOfUnits > 1) { + pricePeriod = priceStr + " / " + numberOfUnits + " " + unitStr; + } + if (originalNumberOfUnits == 3 && subscriptionPeriod.getUnit() == PeriodUnit.MONTH) { + originalPricePeriod = originalPriceStr + " / " + ctx.getString(R.string.months_3).toLowerCase(); + } else if (originalNumberOfUnits > 1) { + originalPricePeriod = originalPriceStr + " / " + originalNumberOfUnits + " " + originalUnitsStr; + } + } + String periodPriceStr = introductoryCycles == 1 ? priceStr : pricePeriod; + + int firstPartRes = totalPeriods == 1 ? R.string.get_discount_first_part : R.string.get_discount_first_few_part; + SpannableStringBuilder mainPart = new SpannableStringBuilder(ctx.getString(firstPartRes, periodPriceStr, getDisountPeriodString(unitStr, totalPeriods))); + SpannableStringBuilder thenPart = new SpannableStringBuilder(ctx.getString(R.string.get_discount_second_part, originalPricePeriod)); + Typeface typefaceRegular = FontCache.getRobotoRegular(ctx); + Typeface typefaceBold = FontCache.getRobotoMedium(ctx); + mainPart.setSpan(new CustomTypefaceSpan(typefaceBold), 0, mainPart.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + int textColor = ContextCompat.getColor(ctx, R.color.white_50_transparent); + thenPart.setSpan(new ForegroundColorSpan(textColor), 0, thenPart.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + thenPart.setSpan(new CustomTypefaceSpan(typefaceRegular), 0, thenPart.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return new SpannableStringBuilder(mainPart).append("\n").append(thenPart); + } + } + public static abstract class InAppSubscription extends InAppPurchase { private Map upgrades = new ConcurrentHashMap<>(); private String skuNoVersion; - private String subscriptionPeriod; + private String subscriptionPeriodString; + private Period subscriptionPeriod; private boolean upgrade = false; + private InAppSubscriptionIntroductoryInfo introductoryInfo; + InAppSubscription(@NonNull String skuNoVersion, int version) { super(skuNoVersion + "_v" + version); this.skuNoVersion = skuNoVersion; @@ -482,12 +681,36 @@ public class InAppPurchases { return skuNoVersion; } - public String getSubscriptionPeriod() { + @Nullable + public String getSubscriptionPeriodString() { + return subscriptionPeriodString; + } + + @Nullable + public Period getSubscriptionPeriod() { return subscriptionPeriod; } - public void setSubscriptionPeriod(String subscriptionPeriod) { - this.subscriptionPeriod = subscriptionPeriod; + public void setSubscriptionPeriodString(String subscriptionPeriodString) throws ParseException { + this.subscriptionPeriodString = subscriptionPeriodString; + this.subscriptionPeriod = Period.parse(subscriptionPeriodString); + } + + public InAppSubscriptionIntroductoryInfo getIntroductoryInfo() { + /* + try { + if (subscriptionPeriod != null && subscriptionPeriod.getUnit() == PeriodUnit.YEAR) { + introductoryInfo = new InAppSubscriptionIntroductoryInfo(this, "30 грн.", 30000000L, "P1Y", "1"); + } + } catch (ParseException e) { + // + } + */ + return introductoryInfo; + } + + public void setIntroductoryInfo(InAppSubscriptionIntroductoryInfo introductoryInfo) { + this.introductoryInfo = introductoryInfo; } @Override @@ -504,12 +727,34 @@ public class InAppPurchases { } } + public CharSequence getDescription(@NonNull Context ctx, @Nullable InAppSubscription monthlyLiveUpdates) { + CharSequence descr = getDescription(ctx); + int discountPercent = getDiscountPercent(monthlyLiveUpdates); + return discountPercent > 0 ? ctx.getString(R.string.price_and_discount, descr, discountPercent + "%") : descr; + } + public CharSequence getRenewDescription(@NonNull Context ctx) { return ""; } @Nullable protected abstract InAppSubscription newInstance(@NonNull String sku); + + public int getDiscountPercent(@Nullable InAppSubscription monthlyLiveUpdates) { + double monthlyPriceValue = getMonthlyPriceValue(); + if (monthlyLiveUpdates != null) { + double regularMonthlyPrice = monthlyLiveUpdates.getPriceValue(); + if (regularMonthlyPrice > 0 && monthlyPriceValue > 0 && monthlyPriceValue < regularMonthlyPrice) { + return (int) ((1 - monthlyPriceValue / regularMonthlyPrice) * 100d); + } + } else if (introductoryInfo != null) { + double introductoryMonthlyPrice = introductoryInfo.getIntroductoryMonthlyPriceValue(); + if (introductoryMonthlyPrice > 0 && monthlyPriceValue > 0 && monthlyPriceValue > introductoryMonthlyPrice) { + return (int) ((1 - introductoryMonthlyPrice / monthlyPriceValue) * 100d); + } + } + return 0; + } } public static class InAppPurchaseFullVersion extends InAppPurchase { @@ -623,14 +868,6 @@ public class InAppPurchases { return ctx.getString(R.string.osm_live_payment_monthly_title); } - @Override - public CharSequence getDescription(@NonNull Context ctx) { - CharSequence descr = super.getDescription(ctx); - SpannableStringBuilder text = new SpannableStringBuilder(descr).append(". ").append(ctx.getString(R.string.osm_live_payment_contribute_descr)); - text.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), descr.length() + 1, text.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - return text; - } - @Override public CharSequence getRenewDescription(@NonNull Context ctx) { return ctx.getString(R.string.osm_live_payment_renews_monthly);