Fix merge
This commit is contained in:
parent
ef52f119d7
commit
30056e5dea
10 changed files with 596 additions and 182 deletions
171
OsmAnd-java/src/main/java/net/osmand/Period.java
Normal file
171
OsmAnd-java/src/main/java/net/osmand/Period.java
Normal file
|
@ -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}.
|
||||||
|
* <p>
|
||||||
|
* This will parse the string produced by {@code toString()} which is
|
||||||
|
* based on the ISO-8601 period formats {@code PnY PnM PnD PnW}.
|
||||||
|
* <p>
|
||||||
|
* The string cannot start with negative sign.
|
||||||
|
* The ASCII letter "P" is next in upper or lower case.
|
||||||
|
* <p>
|
||||||
|
* For example, the following are valid inputs:
|
||||||
|
* <pre>
|
||||||
|
* "P2Y" -- Period.ofYears(2)
|
||||||
|
* "P3M" -- Period.ofMonths(3)
|
||||||
|
* "P4W" -- Period.ofWeeks(4)
|
||||||
|
* "P5D" -- Period.ofDays(5)
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,35 +6,28 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="1dp"
|
android:layout_marginLeft="1dp"
|
||||||
android:layout_marginRight="1dp"
|
android:layout_marginRight="1dp"
|
||||||
android:background="?attr/subscription_active_bg_color"
|
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/div_top"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="?attr/subscription_active_div_color"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_container"
|
android:id="@+id/button_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
android:minHeight="@dimen/dialog_button_ex_height"
|
android:orientation="vertical"
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingLeft="@dimen/card_padding"
|
android:paddingLeft="@dimen/card_padding"
|
||||||
android:paddingRight="@dimen/card_padding">
|
android:paddingRight="@dimen/card_padding">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginTop="@dimen/list_header_padding"
|
android:layout_marginTop="@dimen/list_header_padding"
|
||||||
android:layout_marginEnd="@dimen/list_content_padding"
|
|
||||||
android:layout_marginRight="@dimen/list_content_padding"
|
|
||||||
android:layout_marginBottom="@dimen/list_header_padding"
|
android:layout_marginBottom="@dimen/list_header_padding"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
@ -48,10 +41,16 @@
|
||||||
osmand:typeface="@string/font_roboto_regular"
|
osmand:typeface="@string/font_roboto_regular"
|
||||||
tools:text="Pay monthly" />
|
tools:text="Pay monthly" />
|
||||||
|
|
||||||
<LinearLayout
|
<net.osmand.plus.widgets.TextViewEx
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/description_contribute"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
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" />
|
||||||
|
|
||||||
<net.osmand.plus.widgets.TextViewEx
|
<net.osmand.plus.widgets.TextViewEx
|
||||||
android:id="@+id/description"
|
android:id="@+id/description"
|
||||||
|
@ -64,11 +63,20 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<android.support.v7.widget.AppCompatImageView
|
||||||
|
android:id="@+id/right_image"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="@dimen/list_header_padding"
|
||||||
|
android:layout_marginLeft="@dimen/list_header_padding"
|
||||||
|
android:src="@drawable/ic_action_singleselect" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_view"
|
android:id="@+id/button_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="?attr/wikivoyage_secondary_btn_bg">
|
android:background="?attr/wikivoyage_secondary_btn_bg">
|
||||||
|
@ -76,7 +84,7 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button"
|
android:id="@+id/button"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/dialog_button_height"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="@dimen/list_header_padding">
|
android:padding="@dimen/list_header_padding">
|
||||||
|
@ -100,7 +108,7 @@
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_cancel_view"
|
android:id="@+id/button_cancel_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="?attr/wikivoyage_primary_btn_bg">
|
android:background="?attr/wikivoyage_primary_btn_bg">
|
||||||
|
@ -108,7 +116,8 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_cancel"
|
android:id="@+id/button_cancel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/dialog_button_height"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="@dimen/list_header_padding">
|
android:padding="@dimen/list_header_padding">
|
||||||
|
@ -121,7 +130,7 @@
|
||||||
android:letterSpacing="@dimen/text_button_letter_spacing"
|
android:letterSpacing="@dimen/text_button_letter_spacing"
|
||||||
android:maxWidth="@dimen/dialog_button_ex_max_width"
|
android:maxWidth="@dimen/dialog_button_ex_max_width"
|
||||||
android:minWidth="@dimen/dialog_button_ex_min_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:textColor="@color/color_white"
|
||||||
android:textSize="@dimen/text_button_text_size"
|
android:textSize="@dimen/text_button_text_size"
|
||||||
osmand:typeface="@string/font_roboto_medium" />
|
osmand:typeface="@string/font_roboto_medium" />
|
||||||
|
@ -136,16 +145,8 @@
|
||||||
android:id="@+id/div"
|
android:id="@+id/div"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_marginLeft="@dimen/card_padding"
|
android:layout_marginTop="@dimen/content_padding_small"
|
||||||
android:layout_marginRight="@dimen/card_padding"
|
android:background="?attr/wikivoyage_card_divider_color"
|
||||||
android:background="?attr/subscription_active_div_color"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/div_bottom"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="?attr/subscription_active_div_color"
|
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -12,82 +12,52 @@
|
||||||
android:id="@+id/button_container"
|
android:id="@+id/button_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
android:minHeight="@dimen/dialog_button_ex_height"
|
android:orientation="vertical"
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingLeft="@dimen/card_padding"
|
android:paddingLeft="@dimen/card_padding"
|
||||||
android:paddingRight="@dimen/card_padding">
|
android:paddingRight="@dimen/card_padding">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginTop="@dimen/list_header_padding"
|
android:layout_marginTop="@dimen/list_header_padding"
|
||||||
android:layout_marginEnd="@dimen/list_content_padding"
|
|
||||||
android:layout_marginRight="@dimen/list_content_padding"
|
|
||||||
android:layout_marginBottom="@dimen/list_header_padding"
|
android:layout_marginBottom="@dimen/list_header_padding"
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<net.osmand.plus.widgets.TextViewEx
|
<net.osmand.plus.widgets.TextViewEx
|
||||||
android:id="@+id/title"
|
android:id="@+id/title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
|
||||||
android:textColor="?attr/dialog_title_color"
|
android:textColor="?attr/dialog_title_color"
|
||||||
android:textSize="@dimen/default_list_text_size"
|
android:textSize="@dimen/default_list_text_size"
|
||||||
osmand:typeface="@string/font_roboto_regular"
|
osmand:typeface="@string/font_roboto_regular"
|
||||||
tools:text="Pay monthly" />
|
tools:text="Pay monthly" />
|
||||||
|
|
||||||
<LinearLayout
|
<net.osmand.plus.widgets.TextViewEx
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/description_contribute"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
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" />
|
||||||
|
|
||||||
<net.osmand.plus.widgets.TextViewEx
|
<net.osmand.plus.widgets.TextViewEx
|
||||||
android:id="@+id/description"
|
android:id="@+id/description"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
|
||||||
android:textColor="?attr/card_description_text_color"
|
android:textColor="?attr/card_description_text_color"
|
||||||
android:textSize="@dimen/default_desc_text_size"
|
android:textSize="@dimen/default_desc_text_size"
|
||||||
osmand:typeface="@string/font_roboto_regular"
|
osmand:typeface="@string/font_roboto_regular"
|
||||||
tools:text="Monthly payment" />
|
tools:text="$0.62 / month • Save 68%" />
|
||||||
|
|
||||||
<net.osmand.plus.widgets.TextViewEx
|
|
||||||
android:id="@+id/discount_banner_regular"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:layout_marginStart="@dimen/list_item_button_padding"
|
|
||||||
android:layout_marginLeft="@dimen/list_item_button_padding"
|
|
||||||
android:background="?attr/text_rounded_bg_regular"
|
|
||||||
android:textColor="?attr/card_description_text_color"
|
|
||||||
android:textSize="@dimen/default_desc_text_size"
|
|
||||||
osmand:typeface="@string/font_roboto_regular"
|
|
||||||
tools:text=" Save 20%! " />
|
|
||||||
|
|
||||||
<net.osmand.plus.widgets.TextViewEx
|
|
||||||
android:id="@+id/discount_banner_active"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:layout_marginStart="@dimen/list_item_button_padding"
|
|
||||||
android:layout_marginLeft="@dimen/list_item_button_padding"
|
|
||||||
android:background="?attr/text_rounded_bg_active"
|
|
||||||
android:textColor="@color/osmand_orange"
|
|
||||||
android:textSize="@dimen/default_desc_text_size"
|
|
||||||
osmand:typeface="@string/font_roboto_regular"
|
|
||||||
tools:text=" Save 20%! " />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_view"
|
android:id="@+id/button_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="?attr/btn_round_border_2">
|
android:background="?attr/btn_round_border_2">
|
||||||
|
@ -95,7 +65,7 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button"
|
android:id="@+id/button"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/dialog_button_height"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackground"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
@ -119,7 +89,7 @@
|
||||||
android:textColor="?attr/color_dialog_buttons"
|
android:textColor="?attr/color_dialog_buttons"
|
||||||
android:textSize="@dimen/text_button_text_size"
|
android:textSize="@dimen/text_button_text_size"
|
||||||
osmand:typeface="@string/font_roboto_medium"
|
osmand:typeface="@string/font_roboto_medium"
|
||||||
tools:text="7,99€" />
|
tools:text="7,99€ / year" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -127,7 +97,7 @@
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_ex_view"
|
android:id="@+id/button_ex_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="?attr/wikivoyage_primary_btn_bg">
|
android:background="?attr/wikivoyage_primary_btn_bg">
|
||||||
|
@ -135,7 +105,7 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_ex"
|
android:id="@+id/button_ex"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/dialog_button_height"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackground"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
@ -152,7 +122,7 @@
|
||||||
android:textColor="@color/color_white"
|
android:textColor="@color/color_white"
|
||||||
android:textSize="@dimen/text_button_text_size"
|
android:textSize="@dimen/text_button_text_size"
|
||||||
osmand:typeface="@string/font_roboto_medium"
|
osmand:typeface="@string/font_roboto_medium"
|
||||||
tools:text="7,99€" />
|
tools:text="$3.99 for six month\nthan $7.49 / year" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -164,8 +134,7 @@
|
||||||
android:id="@+id/div"
|
android:id="@+id/div"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_marginLeft="@dimen/card_padding"
|
android:layout_marginTop="@dimen/content_padding_small"
|
||||||
android:layout_marginRight="@dimen/card_padding"
|
|
||||||
android:background="?attr/wikivoyage_card_divider_color"
|
android:background="?attr/wikivoyage_card_divider_color"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="2dp"
|
android:layout_marginBottom="2dp"
|
||||||
|
android:paddingBottom="@dimen/content_padding_small"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,26 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<string name="day">День</string>
|
||||||
|
<string name="days_2_4">Дня</string>
|
||||||
|
<string name="days_5">Дней</string>
|
||||||
|
<string name="week">Неделя</string>
|
||||||
|
<string name="weeks_2_4">Недели</string>
|
||||||
|
<string name="weeks_5">Недель</string>
|
||||||
|
<string name="month">Месяц</string>
|
||||||
|
<string name="months_2_4">Месяца</string>
|
||||||
|
<string name="months_5">Месяцев</string>
|
||||||
|
<string name="year">Год</string>
|
||||||
|
<string name="years_2_4">Года</string>
|
||||||
|
<string name="years_5">Лет</string>
|
||||||
|
<string name="months_3">Три месяца</string>
|
||||||
|
<string name="price_free">Бесплатно</string>
|
||||||
|
<string name="get_discount_title">Получите %1$d %2$s со скидкой %3$s.</string>
|
||||||
|
<string name="get_free_trial_title">Начать бесплатный период в %1$d %2$s.</string>
|
||||||
|
<string name="get_discount_first_part">%1$s за первый %2$s</string>
|
||||||
|
<string name="get_discount_first_few_part">%1$s за первые %2$s</string>
|
||||||
|
<string name="get_discount_second_part">затем %1$s</string>
|
||||||
|
<string name="cancel_subscription">Отменить подписку</string>
|
||||||
|
<string name="price_and_discount">%1$s • Экономия %2$s</string>
|
||||||
<string name="rendering_attr_winter_road_name">Автозимник</string>
|
<string name="rendering_attr_winter_road_name">Автозимник</string>
|
||||||
<string name="rendering_attr_ice_road_name">Ледовый автозимник</string>
|
<string name="rendering_attr_ice_road_name">Ледовый автозимник</string>
|
||||||
<string name="routeInfo_winter_ice_road_name">Зимники</string>
|
<string name="routeInfo_winter_ice_road_name">Зимники</string>
|
||||||
|
|
|
@ -11,6 +11,30 @@
|
||||||
Thx - Hardy
|
Thx - Hardy
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
<string name="day">Day</string>
|
||||||
|
<string name="days_2_4">Days</string>
|
||||||
|
<string name="days_5">Days</string>
|
||||||
|
<string name="week">Week</string>
|
||||||
|
<string name="weeks_2_4">Weeks</string>
|
||||||
|
<string name="weeks_5">Weeks</string>
|
||||||
|
<string name="month">Month</string>
|
||||||
|
<string name="months_2_4">Months</string>
|
||||||
|
<string name="months_5">Months</string>
|
||||||
|
<string name="year">Year</string>
|
||||||
|
<string name="years_2_4">Years</string>
|
||||||
|
<string name="years_5">Years</string>
|
||||||
|
<string name="months_3">Three months</string>
|
||||||
|
<string name="price_free">Free</string>
|
||||||
|
<string name="get_discount_title">Get %1$d %2$s at %3$s off.</string>
|
||||||
|
<string name="get_free_trial_title">Start your %1$d %2$s free trial.</string>
|
||||||
|
<string name="get_discount_first_part">%1$s for first %2$s</string>
|
||||||
|
<string name="get_discount_first_few_part">%1$s for first %2$s</string>
|
||||||
|
<string name="get_discount_second_part">then %1$s</string>
|
||||||
|
<string name="cancel_subscription">Cancel subscription</string>
|
||||||
|
<string name="price_and_discount">%1$s • Save %2$s</string>
|
||||||
|
<string name="app_mode_wagon">Wagon</string>
|
||||||
|
<string name="app_mode_pickup_truck">Pickup truck</string>
|
||||||
|
<string name="shared_string_default">Default</string>
|
||||||
<string name="gpx_join_gaps">Join gaps</string>
|
<string name="gpx_join_gaps">Join gaps</string>
|
||||||
<string name="app_mode_camper">Camper</string>
|
<string name="app_mode_camper">Camper</string>
|
||||||
<string name="app_mode_campervan">Campervan</string>
|
<string name="app_mode_campervan">Campervan</string>
|
||||||
|
|
|
@ -30,6 +30,9 @@ import android.support.annotation.NonNull;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v4.content.FileProvider;
|
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.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
|
@ -60,6 +63,7 @@ import java.util.Date;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static android.content.Context.POWER_SERVICE;
|
import static android.content.Context.POWER_SERVICE;
|
||||||
|
@ -576,4 +580,8 @@ public class AndroidUtils {
|
||||||
return baseString;
|
return baseString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isRTL() {
|
||||||
|
return TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseListener;
|
||||||
import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseTaskType;
|
import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseTaskType;
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppPurchase;
|
import net.osmand.plus.inapp.InAppPurchases.InAppPurchase;
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppSubscription;
|
import net.osmand.plus.inapp.InAppPurchases.InAppSubscription;
|
||||||
|
import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionIntroductoryInfo;
|
||||||
import net.osmand.plus.liveupdates.SubscriptionFragment;
|
import net.osmand.plus.liveupdates.SubscriptionFragment;
|
||||||
import net.osmand.plus.srtmplugin.SRTMPlugin;
|
import net.osmand.plus.srtmplugin.SRTMPlugin;
|
||||||
import net.osmand.plus.widgets.TextViewEx;
|
import net.osmand.plus.widgets.TextViewEx;
|
||||||
|
@ -341,38 +342,40 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment
|
||||||
} else if (osmLiveCardButtonsContainer != null) {
|
} else if (osmLiveCardButtonsContainer != null) {
|
||||||
osmLiveCardButtonsContainer.removeAllViews();
|
osmLiveCardButtonsContainer.removeAllViews();
|
||||||
View lastBtn = null;
|
View lastBtn = null;
|
||||||
InAppSubscription monthlyLiveUpdates = purchaseHelper.getMonthlyLiveUpdates();
|
|
||||||
double regularMonthlyPrice = monthlyLiveUpdates.getPriceValue();
|
|
||||||
List<InAppSubscription> visibleSubscriptions = purchaseHelper.getLiveUpdates().getVisibleSubscriptions();
|
List<InAppSubscription> visibleSubscriptions = purchaseHelper.getLiveUpdates().getVisibleSubscriptions();
|
||||||
boolean anyPurchased = false;
|
boolean anyPurchasedOrIntroducted = false;
|
||||||
for (final InAppSubscription s : visibleSubscriptions) {
|
for (final InAppSubscription s : visibleSubscriptions) {
|
||||||
if (s.isPurchased()) {
|
if (s.isPurchased() || s.getIntroductoryInfo() != null) {
|
||||||
anyPurchased = true;
|
anyPurchasedOrIntroducted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final InAppSubscription s : visibleSubscriptions) {
|
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()) {
|
if (s.isPurchased()) {
|
||||||
View buttonPurchased = inflate(R.layout.purchase_dialog_card_button_active_ex, osmLiveCardButtonsContainer);
|
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 title = (TextViewEx) buttonPurchased.findViewById(R.id.title);
|
||||||
TextViewEx description = (TextViewEx) buttonPurchased.findViewById(R.id.description);
|
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);
|
TextViewEx buttonTitle = (TextViewEx) buttonPurchased.findViewById(R.id.button_title);
|
||||||
View buttonView = buttonPurchased.findViewById(R.id.button_view);
|
View buttonView = buttonPurchased.findViewById(R.id.button_view);
|
||||||
View buttonCancelView = buttonPurchased.findViewById(R.id.button_cancel_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);
|
View div = buttonPurchased.findViewById(R.id.div);
|
||||||
|
AppCompatImageView rightImage = buttonPurchased.findViewById(R.id.right_image);
|
||||||
|
|
||||||
title.setText(s.getTitle(ctx));
|
title.setText(s.getTitle(ctx));
|
||||||
description.setText(s.getDescription(ctx));
|
description.setText(descriptionText);
|
||||||
buttonTitle.setText(s.getPrice(ctx));
|
buttonTitle.setText(priceTitle);
|
||||||
buttonView.setVisibility(View.VISIBLE);
|
buttonView.setVisibility(View.VISIBLE);
|
||||||
buttonCancelView.setVisibility(View.GONE);
|
buttonCancelView.setVisibility(View.GONE);
|
||||||
buttonPurchased.setOnClickListener(null);
|
buttonPurchased.setOnClickListener(null);
|
||||||
divTop.setVisibility(View.VISIBLE);
|
div.setVisibility(View.GONE);
|
||||||
div.setVisibility(View.VISIBLE);
|
rightImage.setVisibility(View.GONE);
|
||||||
divBottom.setVisibility(View.GONE);
|
|
||||||
if (s.isDonationSupported()) {
|
if (s.isDonationSupported()) {
|
||||||
buttonPurchased.setOnClickListener(new OnClickListener() {
|
buttonPurchased.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -387,14 +390,12 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment
|
||||||
osmLiveCardButtonsContainer.addView(buttonPurchased);
|
osmLiveCardButtonsContainer.addView(buttonPurchased);
|
||||||
|
|
||||||
View buttonCancel = inflate(R.layout.purchase_dialog_card_button_active_ex, osmLiveCardButtonsContainer);
|
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);
|
title = (TextViewEx) buttonCancel.findViewById(R.id.title);
|
||||||
description = (TextViewEx) buttonCancel.findViewById(R.id.description);
|
description = (TextViewEx) buttonCancel.findViewById(R.id.description);
|
||||||
buttonView = buttonCancel.findViewById(R.id.button_view);
|
buttonView = buttonCancel.findViewById(R.id.button_view);
|
||||||
buttonCancelView = buttonCancel.findViewById(R.id.button_cancel_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);
|
div = buttonCancel.findViewById(R.id.div);
|
||||||
|
rightImage = buttonPurchased.findViewById(R.id.right_image);
|
||||||
|
|
||||||
title.setText(getString(R.string.osm_live_payment_current_subscription));
|
title.setText(getString(R.string.osm_live_payment_current_subscription));
|
||||||
description.setText(s.getRenewDescription(ctx));
|
description.setText(s.getRenewDescription(ctx));
|
||||||
|
@ -406,70 +407,34 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment
|
||||||
manageSubscription(ctx, s.getSku());
|
manageSubscription(ctx, s.getSku());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
divTop.setVisibility(View.GONE);
|
div.setVisibility(View.VISIBLE);
|
||||||
div.setVisibility(View.GONE);
|
rightImage.setVisibility(View.VISIBLE);
|
||||||
divBottom.setVisibility(View.VISIBLE);
|
|
||||||
osmLiveCardButtonsContainer.addView(buttonCancel);
|
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;
|
lastBtn = buttonCancel;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
View button = inflate(R.layout.purchase_dialog_card_button_ex, osmLiveCardButtonsContainer);
|
View button = inflate(R.layout.purchase_dialog_card_button_ex, osmLiveCardButtonsContainer);
|
||||||
TextViewEx title = (TextViewEx) button.findViewById(R.id.title);
|
TextViewEx title = (TextViewEx) button.findViewById(R.id.title);
|
||||||
TextViewEx description = (TextViewEx) button.findViewById(R.id.description);
|
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 buttonView = button.findViewById(R.id.button_view);
|
||||||
View buttonExView = button.findViewById(R.id.button_ex_view);
|
View buttonExView = button.findViewById(R.id.button_ex_view);
|
||||||
TextViewEx buttonTitle = (TextViewEx) button.findViewById(R.id.button_title);
|
TextViewEx buttonTitle = (TextViewEx) button.findViewById(R.id.button_title);
|
||||||
TextViewEx buttonExTitle = (TextViewEx) button.findViewById(R.id.button_ex_title);
|
TextViewEx buttonExTitle = (TextViewEx) button.findViewById(R.id.button_ex_title);
|
||||||
buttonView.setVisibility(anyPurchased ? View.VISIBLE : View.GONE);
|
boolean showSolidButton = !anyPurchasedOrIntroducted || hasIntroductoryInfo;
|
||||||
buttonExView.setVisibility(!anyPurchased ? View.VISIBLE : View.GONE);
|
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);
|
View div = button.findViewById(R.id.div);
|
||||||
|
|
||||||
title.setText(s.getTitle(ctx));
|
title.setText(s.getTitle(ctx));
|
||||||
description.setText(s.getDescription(ctx));
|
description.setText(descriptionText);
|
||||||
buttonTitle.setText(s.getPrice(ctx));
|
buttonTitle.setText(priceTitle);
|
||||||
buttonExTitle.setText(s.getPrice(ctx));
|
buttonExTitle.setText(priceTitle);
|
||||||
|
|
||||||
if (regularMonthlyPrice > 0 && s.getMonthlyPriceValue() > 0 && s.getMonthlyPriceValue() < regularMonthlyPrice) {
|
if (!showSolidButton) {
|
||||||
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) {
|
|
||||||
buttonView.setOnClickListener(new OnClickListener() {
|
buttonView.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
@ -494,10 +459,6 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment
|
||||||
if (div != null) {
|
if (div != null) {
|
||||||
div.setVisibility(View.GONE);
|
div.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
View divBottom = lastBtn.findViewById(R.id.div_bottom);
|
|
||||||
if (divBottom != null) {
|
|
||||||
divBottom.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (osmLiveCardProgress != null) {
|
if (osmLiveCardProgress != null) {
|
||||||
osmLiveCardProgress.setVisibility(View.GONE);
|
osmLiveCardProgress.setVisibility(View.GONE);
|
||||||
|
|
|
@ -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.InAppPurchase.PurchaseState;
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription;
|
import net.osmand.plus.inapp.InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription;
|
||||||
import net.osmand.plus.inapp.InAppPurchases.InAppSubscription;
|
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.InAppPurchases.InAppSubscriptionList;
|
||||||
import net.osmand.plus.inapp.util.BillingManager;
|
import net.osmand.plus.inapp.util.BillingManager;
|
||||||
import net.osmand.plus.inapp.util.BillingManager.BillingUpdatesListener;
|
import net.osmand.plus.inapp.util.BillingManager.BillingUpdatesListener;
|
||||||
|
@ -39,6 +40,7 @@ import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -603,7 +605,26 @@ public class InAppPurchaseHelper {
|
||||||
String subscriptionPeriod = skuDetails.getSubscriptionPeriod();
|
String subscriptionPeriod = skuDetails.getSubscriptionPeriod();
|
||||||
if (!Algorithms.isEmpty(subscriptionPeriod)) {
|
if (!Algorithms.isEmpty(subscriptionPeriod)) {
|
||||||
if (inAppPurchase instanceof InAppSubscription) {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
package net.osmand.plus.inapp;
|
package net.osmand.plus.inapp;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Typeface;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
|
||||||
import com.android.billingclient.api.SkuDetails;
|
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.OsmandApplication;
|
||||||
import net.osmand.plus.R;
|
import net.osmand.plus.R;
|
||||||
import net.osmand.plus.Version;
|
import net.osmand.plus.Version;
|
||||||
|
import net.osmand.plus.helpers.FontCache;
|
||||||
|
import net.osmand.plus.widgets.style.CustomTypefaceSpan;
|
||||||
import net.osmand.util.Algorithms;
|
import net.osmand.util.Algorithms;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
|
@ -169,7 +177,7 @@ public class InAppPurchases {
|
||||||
private List<InAppSubscription> subscriptions;
|
private List<InAppSubscription> subscriptions;
|
||||||
|
|
||||||
InAppSubscriptionList(@NonNull InAppSubscription[] subscriptionsArray) {
|
InAppSubscriptionList(@NonNull InAppSubscription[] subscriptionsArray) {
|
||||||
this.subscriptions = Arrays.asList(subscriptionsArray);;
|
this.subscriptions = Arrays.asList(subscriptionsArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<InAppSubscription> getSubscriptions() {
|
private List<InAppSubscription> 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 {
|
public static abstract class InAppSubscription extends InAppPurchase {
|
||||||
|
|
||||||
private Map<String, InAppSubscription> upgrades = new ConcurrentHashMap<>();
|
private Map<String, InAppSubscription> upgrades = new ConcurrentHashMap<>();
|
||||||
private String skuNoVersion;
|
private String skuNoVersion;
|
||||||
private String subscriptionPeriod;
|
private String subscriptionPeriodString;
|
||||||
|
private Period subscriptionPeriod;
|
||||||
private boolean upgrade = false;
|
private boolean upgrade = false;
|
||||||
|
|
||||||
|
private InAppSubscriptionIntroductoryInfo introductoryInfo;
|
||||||
|
|
||||||
InAppSubscription(@NonNull String skuNoVersion, int version) {
|
InAppSubscription(@NonNull String skuNoVersion, int version) {
|
||||||
super(skuNoVersion + "_v" + version);
|
super(skuNoVersion + "_v" + version);
|
||||||
this.skuNoVersion = skuNoVersion;
|
this.skuNoVersion = skuNoVersion;
|
||||||
|
@ -482,12 +681,36 @@ public class InAppPurchases {
|
||||||
return skuNoVersion;
|
return skuNoVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSubscriptionPeriod() {
|
@Nullable
|
||||||
|
public String getSubscriptionPeriodString() {
|
||||||
|
return subscriptionPeriodString;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Period getSubscriptionPeriod() {
|
||||||
return subscriptionPeriod;
|
return subscriptionPeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSubscriptionPeriod(String subscriptionPeriod) {
|
public void setSubscriptionPeriodString(String subscriptionPeriodString) throws ParseException {
|
||||||
this.subscriptionPeriod = subscriptionPeriod;
|
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
|
@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) {
|
public CharSequence getRenewDescription(@NonNull Context ctx) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
protected abstract InAppSubscription newInstance(@NonNull String sku);
|
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 {
|
public static class InAppPurchaseFullVersion extends InAppPurchase {
|
||||||
|
@ -623,14 +868,6 @@ public class InAppPurchases {
|
||||||
return ctx.getString(R.string.osm_live_payment_monthly_title);
|
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
|
@Override
|
||||||
public CharSequence getRenewDescription(@NonNull Context ctx) {
|
public CharSequence getRenewDescription(@NonNull Context ctx) {
|
||||||
return ctx.getString(R.string.osm_live_payment_renews_monthly);
|
return ctx.getString(R.string.osm_live_payment_renews_monthly);
|
||||||
|
|
Loading…
Reference in a new issue