Compare commits

...

35 commits
master ... r3.4

Author SHA1 Message Date
max-klaus
fa5425c8fe Fix huawei build 2019-09-12 19:13:58 +03:00
max-klaus
8352fa4d84 Revert huawei build 2019-09-11 21:59:22 +03:00
max-klaus
ad4fa9c500 Try to fix build 2019-09-11 21:20:38 +03:00
max-klaus
fae21cbdf3 Try to fix build 2019-09-11 21:13:56 +03:00
max-klaus
784ca0df66 Try to fix build 2019-09-11 21:11:40 +03:00
max-klaus
d2ea48e468 Fix huawei build 2019-09-11 20:27:45 +03:00
max-klaus
98162d6b8f Fixed Huawei build 2019-09-11 19:30:36 +03:00
max-klaus
1505b77fea Fix build 2019-09-11 19:30:26 +03:00
Nazar
184c7d3b90 huawei drm integration p.6
remove drm-strings
2019-09-11 19:30:03 +03:00
Nazar
556d75c6f2 huawei drm integration p.5 2019-09-11 19:29:33 +03:00
Nazar
5bc0da0efb huawei drm integration p.4 2019-09-11 19:29:23 +03:00
Nazar
cd4bb2667c huawei drm integration p.3 2019-09-11 19:29:12 +03:00
Nazar
dd296864f4 huawei drm integration p.2 2019-09-11 19:29:00 +03:00
Nazar
1f04ce2ce2 huawei drm integration 2019-09-11 19:28:34 +03:00
max-klaus
3a7232c8b4 Fix #7468 #7460 2019-09-05 11:45:54 +03:00
max-klaus
53acf00f1a Fix #7491 2019-09-01 10:23:31 +03:00
max-klaus
8235f31f7d Fix BillingManager crash 2019-08-20 10:45:57 +03:00
max-klaus
4353251067 Fix billing and choose plan UI 2019-08-18 22:12:05 +03:00
max-klaus
30056e5dea Fix merge 2019-08-18 19:41:44 +03:00
max-klaus
ef52f119d7 Drop billing aidl 2019-08-18 19:39:25 +03:00
max-klaus
222b9e410b Move purchases to google billing library 2019-08-18 19:39:07 +03:00
max-klaus
0808d1a3b8 Added occitan language (configure map) 2019-08-18 19:38:44 +03:00
max-klaus
5eac1ac31d Added occitan language 2019-08-18 19:38:31 +03:00
max-klaus
4e5d5f700f Fix annual monthly price 2019-08-18 19:36:49 +03:00
max-klaus
f0eae1515a Move OLC lib to gradle 2019-08-18 19:35:40 +03:00
max-klaus
1ae1893105 Fix sensor for turn screen on 2019-08-14 15:58:20 +03:00
max-klaus
ceff1bef41 Fix turn screen on and notifications flickering 2019-08-14 15:58:09 +03:00
max-klaus
e04a63987c Fix route simulation in background 2019-08-14 15:57:51 +03:00
max-klaus
e11ee96e6a Fix turn screen on flickering 2019-08-14 15:57:31 +03:00
max-klaus
1d316087b6 Fix context menu point animation 2019-08-13 10:23:18 +03:00
max-klaus
f0dde03f19 Fix turn screen on/off 2019-08-09 12:35:10 +03:00
max-klaus
ba6d3c1c85 Fix jumping animation 2019-08-09 12:33:54 +03:00
max-klaus
5e591194c0 Fix route details dismiss on tap 2019-08-03 22:17:31 +03:00
max-klaus
46b88489a3 Fix android 4 padding 2019-08-03 19:34:13 +03:00
max-klaus
e86bde9c61 Fix transport stop context menu. Fix top route badge press. 2019-08-03 19:18:21 +03:00
48 changed files with 1743 additions and 2558 deletions

View file

@ -103,6 +103,7 @@ dependencies {
implementation 'org.apache.commons:commons-compress:1.17'
implementation 'com.moparisthebest:junidecode:0.1.1'
implementation 'com.vividsolutions:jts-core:1.14.0'
implementation 'com.google.openlocationcode:openlocationcode:1.0.4'
// turn off for now
//implementation 'com.atilika.kuromoji:kuromoji-ipadic:0.9.0'
implementation 'net.sf.kxml:kxml2:2.1.8'

View file

@ -1,618 +0,0 @@
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.openlocationcode;
import java.math.BigDecimal;
/**
* Convert locations to and from convenient short codes.
*
* Open Location Codes are short, ~10 character codes that can be used instead of street
* addresses. The codes can be generated and decoded offline, and use a reduced character set that
* minimises the chance of codes including words.
*
* This provides both object and static methods.
*
* Create an object with:
* OpenLocationCode code = new OpenLocationCode("7JVW52GR+2V");
* OpenLocationCode code = new OpenLocationCode("52GR+2V");
* OpenLocationCode code = new OpenLocationCode(27.175063, 78.042188);
* OpenLocationCode code = new OpenLocationCode(27.175063, 78.042188, 11);
*
* Once you have a code object, you can apply the other methods to it, such as to shorten:
* code.shorten(27.176, 78.05)
*
* Recover the nearest match (if the code was a short code):
* code.recover(27.176, 78.05)
*
* Or decode a code into its coordinates, returning a CodeArea object.
* code.decode()
*
* @author Jiri Semecky
* @author Doug Rinckes
*/
public final class OpenLocationCode {
// Provides a normal precision code, approximately 14x14 meters.
public static final int CODE_PRECISION_NORMAL = 10;
// The character set used to encode the values.
public static final String CODE_ALPHABET = "23456789CFGHJMPQRVWX";
// A separator used to break the code into two parts to aid memorability.
public static final char SEPARATOR = '+';
// The character used to pad codes.
public static final char PADDING_CHARACTER = '0';
// The number of characters to place before the separator.
private static final int SEPARATOR_POSITION = 8;
// The max number of digits to process in a plus code.
public static final int MAX_DIGIT_COUNT = 15;
// Note: The double type can't be used because of the rounding arithmetic due to floating point
// implementation. Eg. "8.95 - 8" can give result 0.9499999999999 instead of 0.95 which
// incorrectly classify the points on the border of a cell. Therefore all the calculation is done
// using BigDecimal.
// The base to use to convert numbers to/from.
private static final BigDecimal ENCODING_BASE = new BigDecimal(CODE_ALPHABET.length());
// The maximum value for latitude in degrees.
private static final BigDecimal LATITUDE_MAX = new BigDecimal(90);
// The maximum value for longitude in degrees.
private static final BigDecimal LONGITUDE_MAX = new BigDecimal(180);
// Maximum code length using just lat/lng pair encoding.
private static final int PAIR_CODE_LENGTH = 10;
// Number of columns in the grid refinement method.
private static final BigDecimal GRID_COLUMNS = new BigDecimal(4);
// Number of rows in the grid refinement method.
private static final BigDecimal GRID_ROWS = new BigDecimal(5);
/**
* Coordinates of a decoded Open Location Code.
*
* <p>The coordinates include the latitude and longitude of the lower left and upper right corners
* and the center of the bounding box for the area the code represents.
*/
public static class CodeArea {
private final BigDecimal southLatitude;
private final BigDecimal westLongitude;
private final BigDecimal northLatitude;
private final BigDecimal eastLongitude;
public CodeArea(
BigDecimal southLatitude,
BigDecimal westLongitude,
BigDecimal northLatitude,
BigDecimal eastLongitude) {
this.southLatitude = southLatitude;
this.westLongitude = westLongitude;
this.northLatitude = northLatitude;
this.eastLongitude = eastLongitude;
}
public double getSouthLatitude() {
return southLatitude.doubleValue();
}
public double getWestLongitude() {
return westLongitude.doubleValue();
}
public double getLatitudeHeight() {
return northLatitude.subtract(southLatitude).doubleValue();
}
public double getLongitudeWidth() {
return eastLongitude.subtract(westLongitude).doubleValue();
}
public double getCenterLatitude() {
return southLatitude.add(northLatitude).doubleValue() / 2;
}
public double getCenterLongitude() {
return westLongitude.add(eastLongitude).doubleValue() / 2;
}
public double getNorthLatitude() {
return northLatitude.doubleValue();
}
public double getEastLongitude() {
return eastLongitude.doubleValue();
}
}
/** The current code for objects. */
private final String code;
/**
* Creates Open Location Code object for the provided code.
* @param code A valid OLC code. Can be a full code or a shortened code.
* @throws IllegalArgumentException when the passed code is not valid.
*/
public OpenLocationCode(String code) {
if (!isValidCode(code.toUpperCase())) {
throw new IllegalArgumentException(
"The provided code '" + code + "' is not a valid Open Location Code.");
}
this.code = code.toUpperCase();
}
/**
* Creates Open Location Code.
* @param latitude The latitude in decimal degrees.
* @param longitude The longitude in decimal degrees.
* @param codeLength The desired number of digits in the code.
* @throws IllegalArgumentException if the code length is not valid.
*/
public OpenLocationCode(double latitude, double longitude, int codeLength) {
// Limit the maximum number of digits in the code.
codeLength = Math.min(codeLength, MAX_DIGIT_COUNT);
// Check that the code length requested is valid.
if (codeLength < 4 || (codeLength < PAIR_CODE_LENGTH && codeLength % 2 == 1)) {
throw new IllegalArgumentException("Illegal code length " + codeLength);
}
// Ensure that latitude and longitude are valid.
latitude = clipLatitude(latitude);
longitude = normalizeLongitude(longitude);
// Latitude 90 needs to be adjusted to be just less, so the returned code can also be decoded.
if (latitude == LATITUDE_MAX.doubleValue()) {
latitude = latitude - 0.9 * computeLatitudePrecision(codeLength);
}
// Adjust latitude and longitude to be in positive number ranges.
// We add the max values when creating the BigDecimal (as opposed to creating a BigDecimal and
// then adding the value) to be consistent with the other implementations.
BigDecimal remainingLatitude = new BigDecimal(latitude + LATITUDE_MAX.doubleValue());
BigDecimal remainingLongitude = new BigDecimal(longitude + LONGITUDE_MAX.doubleValue());
// Count how many digits have been created.
int generatedDigits = 0;
// Store the code.
StringBuilder codeBuilder = new StringBuilder();
// The precisions are initially set to ENCODING_BASE^2 because they will be immediately
// divided.
BigDecimal latPrecision = ENCODING_BASE.multiply(ENCODING_BASE);
BigDecimal lngPrecision = ENCODING_BASE.multiply(ENCODING_BASE);
while (generatedDigits < codeLength) {
if (generatedDigits < PAIR_CODE_LENGTH) {
// Use the normal algorithm for the first set of digits.
latPrecision = latPrecision.divide(ENCODING_BASE);
lngPrecision = lngPrecision.divide(ENCODING_BASE);
BigDecimal latDigit = remainingLatitude.divide(latPrecision, 0, BigDecimal.ROUND_FLOOR);
BigDecimal lngDigit = remainingLongitude.divide(lngPrecision, 0, BigDecimal.ROUND_FLOOR);
remainingLatitude = remainingLatitude.subtract(latPrecision.multiply(latDigit));
remainingLongitude = remainingLongitude.subtract(lngPrecision.multiply(lngDigit));
codeBuilder.append(CODE_ALPHABET.charAt(latDigit.intValue()));
codeBuilder.append(CODE_ALPHABET.charAt(lngDigit.intValue()));
generatedDigits += 2;
} else {
// Use the 4x5 grid for remaining digits.
latPrecision = latPrecision.divide(GRID_ROWS);
lngPrecision = lngPrecision.divide(GRID_COLUMNS);
BigDecimal row = remainingLatitude.divide(latPrecision, 0, BigDecimal.ROUND_FLOOR);
BigDecimal col = remainingLongitude.divide(lngPrecision, 0, BigDecimal.ROUND_FLOOR);
remainingLatitude = remainingLatitude.subtract(latPrecision.multiply(row));
remainingLongitude = remainingLongitude.subtract(lngPrecision.multiply(col));
codeBuilder.append(
CODE_ALPHABET.charAt(row.intValue() * GRID_COLUMNS.intValue() + col.intValue()));
generatedDigits += 1;
}
// If we are at the separator position, add the separator.
if (generatedDigits == SEPARATOR_POSITION) {
codeBuilder.append(SEPARATOR);
}
}
// If the generated code is shorter than the separator position, pad the code and add the
// separator.
if (generatedDigits < SEPARATOR_POSITION) {
for (; generatedDigits < SEPARATOR_POSITION; generatedDigits++) {
codeBuilder.append(PADDING_CHARACTER);
}
codeBuilder.append(SEPARATOR);
}
this.code = codeBuilder.toString();
}
/**
* Creates Open Location Code with the default precision length.
* @param latitude The latitude in decimal degrees.
* @param longitude The longitude in decimal degrees.
*/
public OpenLocationCode(double latitude, double longitude) {
this(latitude, longitude, CODE_PRECISION_NORMAL);
}
/**
* Returns the string representation of the code.
*/
public String getCode() {
return code;
}
/**
* Encodes latitude/longitude into 10 digit Open Location Code. This method is equivalent to
* creating the OpenLocationCode object and getting the code from it.
* @param latitude The latitude in decimal degrees.
* @param longitude The longitude in decimal degrees.
*/
public static String encode(double latitude, double longitude) {
return new OpenLocationCode(latitude, longitude).getCode();
}
/**
* Encodes latitude/longitude into Open Location Code of the provided length. This method is
* equivalent to creating the OpenLocationCode object and getting the code from it.
* @param latitude The latitude in decimal degrees.
* @param longitude The longitude in decimal degrees.
*/
public static String encode(double latitude, double longitude, int codeLength) {
return new OpenLocationCode(latitude, longitude, codeLength).getCode();
}
/**
* Decodes {@link OpenLocationCode} object into {@link CodeArea} object encapsulating
* latitude/longitude bounding box.
*/
public CodeArea decode() {
if (!isFullCode(code)) {
throw new IllegalStateException(
"Method decode() could only be called on valid full codes, code was " + code + ".");
}
// Strip padding and separator characters out of the code.
String decoded = code.replace(String.valueOf(SEPARATOR), "")
.replace(String.valueOf(PADDING_CHARACTER), "");
int digit = 0;
// The precisions are initially set to ENCODING_BASE^2 because they will be immediately
// divided.
BigDecimal latPrecision = ENCODING_BASE.multiply(ENCODING_BASE);
BigDecimal lngPrecision = ENCODING_BASE.multiply(ENCODING_BASE);
// Save the coordinates.
BigDecimal southLatitude = BigDecimal.ZERO;
BigDecimal westLongitude = BigDecimal.ZERO;
// Decode the digits.
while (digit < Math.min(decoded.length(), MAX_DIGIT_COUNT)) {
if (digit < PAIR_CODE_LENGTH) {
// Decode a pair of digits, the first being latitude and the second being longitude.
latPrecision = latPrecision.divide(ENCODING_BASE);
lngPrecision = lngPrecision.divide(ENCODING_BASE);
int digitVal = CODE_ALPHABET.indexOf(decoded.charAt(digit));
southLatitude = southLatitude.add(latPrecision.multiply(new BigDecimal(digitVal)));
digitVal = CODE_ALPHABET.indexOf(decoded.charAt(digit + 1));
westLongitude = westLongitude.add(lngPrecision.multiply(new BigDecimal(digitVal)));
digit += 2;
} else {
// Use the 4x5 grid for digits after 10.
int digitVal = CODE_ALPHABET.indexOf(decoded.charAt(digit));
int row = (int) (digitVal / GRID_COLUMNS.intValue());
int col = digitVal % GRID_COLUMNS.intValue();
latPrecision = latPrecision.divide(GRID_ROWS);
lngPrecision = lngPrecision.divide(GRID_COLUMNS);
southLatitude = southLatitude.add(latPrecision.multiply(new BigDecimal(row)));
westLongitude = westLongitude.add(lngPrecision.multiply(new BigDecimal(col)));
digit += 1;
}
}
return new CodeArea(
southLatitude.subtract(LATITUDE_MAX),
westLongitude.subtract(LONGITUDE_MAX),
southLatitude.subtract(LATITUDE_MAX).add(latPrecision),
westLongitude.subtract(LONGITUDE_MAX).add(lngPrecision));
}
/**
* Decodes code representing Open Location Code into {@link CodeArea} object encapsulating
* latitude/longitude bounding box.
*
* @param code Open Location Code to be decoded.
* @throws IllegalArgumentException if the provided code is not a valid Open Location Code.
*/
public static CodeArea decode(String code) throws IllegalArgumentException {
return new OpenLocationCode(code).decode();
}
/** Returns whether this {@link OpenLocationCode} is a full Open Location Code. */
public boolean isFull() {
return code.indexOf(SEPARATOR) == SEPARATOR_POSITION;
}
/** Returns whether the provided Open Location Code is a full Open Location Code. */
public static boolean isFull(String code) throws IllegalArgumentException {
return new OpenLocationCode(code).isFull();
}
/** Returns whether this {@link OpenLocationCode} is a short Open Location Code. */
public boolean isShort() {
return code.indexOf(SEPARATOR) >= 0 && code.indexOf(SEPARATOR) < SEPARATOR_POSITION;
}
/** Returns whether the provided Open Location Code is a short Open Location Code. */
public static boolean isShort(String code) throws IllegalArgumentException {
return new OpenLocationCode(code).isShort();
}
/**
* Returns whether this {@link OpenLocationCode} is a padded Open Location Code, meaning that it
* contains less than 8 valid digits.
*/
private boolean isPadded() {
return code.indexOf(PADDING_CHARACTER) >= 0;
}
/**
* Returns whether the provided Open Location Code is a padded Open Location Code, meaning that it
* contains less than 8 valid digits.
*/
public static boolean isPadded(String code) throws IllegalArgumentException {
return new OpenLocationCode(code).isPadded();
}
/**
* Returns short {@link OpenLocationCode} from the full Open Location Code created by removing
* four or six digits, depending on the provided reference point. It removes as many digits as
* possible.
*/
public OpenLocationCode shorten(double referenceLatitude, double referenceLongitude) {
if (!isFull()) {
throw new IllegalStateException("shorten() method could only be called on a full code.");
}
if (isPadded()) {
throw new IllegalStateException("shorten() method can not be called on a padded code.");
}
CodeArea codeArea = decode();
double range = Math.max(
Math.abs(referenceLatitude - codeArea.getCenterLatitude()),
Math.abs(referenceLongitude - codeArea.getCenterLongitude()));
// We are going to check to see if we can remove three pairs, two pairs or just one pair of
// digits from the code.
for (int i = 4; i >= 1; i--) {
// Check if we're close enough to shorten. The range must be less than 1/2
// the precision to shorten at all, and we want to allow some safety, so
// use 0.3 instead of 0.5 as a multiplier.
if (range < (computeLatitudePrecision(i * 2) * 0.3)) {
// We're done.
return new OpenLocationCode(code.substring(i * 2));
}
}
throw new IllegalArgumentException(
"Reference location is too far from the Open Location Code center.");
}
/**
* Returns an {@link OpenLocationCode} object representing a full Open Location Code from this
* (short) Open Location Code, given the reference location.
*/
public OpenLocationCode recover(double referenceLatitude, double referenceLongitude) {
if (isFull()) {
// Note: each code is either full xor short, no other option.
return this;
}
referenceLatitude = clipLatitude(referenceLatitude);
referenceLongitude = normalizeLongitude(referenceLongitude);
int digitsToRecover = SEPARATOR_POSITION - code.indexOf(SEPARATOR);
// The precision (height and width) of the missing prefix in degrees.
double prefixPrecision = Math.pow(ENCODING_BASE.intValue(), 2 - (digitsToRecover / 2));
// Use the reference location to generate the prefix.
String recoveredPrefix =
new OpenLocationCode(referenceLatitude, referenceLongitude)
.getCode()
.substring(0, digitsToRecover);
// Combine the prefix with the short code and decode it.
OpenLocationCode recovered = new OpenLocationCode(recoveredPrefix + code);
CodeArea recoveredCodeArea = recovered.decode();
// Work out whether the new code area is too far from the reference location. If it is, we
// move it. It can only be out by a single precision step.
double recoveredLatitude = recoveredCodeArea.getCenterLatitude();
double recoveredLongitude = recoveredCodeArea.getCenterLongitude();
// Move the recovered latitude by one precision up or down if it is too far from the reference,
// unless doing so would lead to an invalid latitude.
double latitudeDiff = recoveredLatitude - referenceLatitude;
if (latitudeDiff > prefixPrecision / 2
&& recoveredLatitude - prefixPrecision > -LATITUDE_MAX.intValue()) {
recoveredLatitude -= prefixPrecision;
} else if (latitudeDiff < -prefixPrecision / 2
&& recoveredLatitude + prefixPrecision < LATITUDE_MAX.intValue()) {
recoveredLatitude += prefixPrecision;
}
// Move the recovered longitude by one precision up or down if it is too far from the
// reference.
double longitudeDiff = recoveredCodeArea.getCenterLongitude() - referenceLongitude;
if (longitudeDiff > prefixPrecision / 2) {
recoveredLongitude -= prefixPrecision;
} else if (longitudeDiff < -prefixPrecision / 2) {
recoveredLongitude += prefixPrecision;
}
return new OpenLocationCode(
recoveredLatitude, recoveredLongitude, recovered.getCode().length() - 1);
}
/**
* Returns whether the bounding box specified by the Open Location Code contains provided point.
*/
public boolean contains(double latitude, double longitude) {
CodeArea codeArea = decode();
return codeArea.getSouthLatitude() <= latitude
&& latitude < codeArea.getNorthLatitude()
&& codeArea.getWestLongitude() <= longitude
&& longitude < codeArea.getEastLongitude();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
OpenLocationCode that = (OpenLocationCode) o;
return code == that.code || code != null && code.equals(that.code);
}
@Override
public int hashCode() {
return code != null ? code.hashCode() : 0;
}
@Override
public String toString() {
return getCode();
}
// Exposed static helper methods.
/** Returns whether the provided string is a valid Open Location code. */
public static boolean isValidCode(String code) {
if (code == null || code.length() < 2) {
return false;
}
code = code.toUpperCase();
// There must be exactly one separator.
int separatorPosition = code.indexOf(SEPARATOR);
if (separatorPosition == -1) {
return false;
}
if (separatorPosition != code.lastIndexOf(SEPARATOR)) {
return false;
}
if (separatorPosition % 2 != 0) {
return false;
}
// Check first two characters: only some values from the alphabet are permitted.
if (separatorPosition == 8) {
// First latitude character can only have first 9 values.
Integer index0 = CODE_ALPHABET.indexOf(code.charAt(0));
if (index0 == null || index0 > 8) {
return false;
}
// First longitude character can only have first 18 values.
Integer index1 = CODE_ALPHABET.indexOf(code.charAt(1));
if (index1 == null || index1 > 17) {
return false;
}
}
// Check the characters before the separator.
boolean paddingStarted = false;
for (int i = 0; i < separatorPosition; i++) {
if (paddingStarted) {
// Once padding starts, there must not be anything but padding.
if (code.charAt(i) != PADDING_CHARACTER) {
return false;
}
continue;
}
if (CODE_ALPHABET.indexOf(code.charAt(i)) != -1) {
continue;
}
if (PADDING_CHARACTER == code.charAt(i)) {
paddingStarted = true;
// Padding can start on even character: 2, 4 or 6.
if (i != 2 && i != 4 && i != 6) {
return false;
}
continue;
}
return false; // Illegal character.
}
// Check the characters after the separator.
if (code.length() > separatorPosition + 1) {
if (paddingStarted) {
return false;
}
// Only one character after separator is forbidden.
if (code.length() == separatorPosition + 2) {
return false;
}
for (int i = separatorPosition + 1; i < code.length(); i++) {
if (CODE_ALPHABET.indexOf(code.charAt(i)) == -1) {
return false;
}
}
}
return true;
}
/** Returns if the code is a valid full Open Location Code. */
public static boolean isFullCode(String code) {
try {
return new OpenLocationCode(code).isFull();
} catch (IllegalArgumentException e) {
return false;
}
}
/** Returns if the code is a valid short Open Location Code. */
public static boolean isShortCode(String code) {
try {
return new OpenLocationCode(code).isShort();
} catch (IllegalArgumentException e) {
return false;
}
}
// Private static methods.
private static double clipLatitude(double latitude) {
return Math.min(Math.max(latitude, -LATITUDE_MAX.intValue()), LATITUDE_MAX.intValue());
}
private static double normalizeLongitude(double longitude) {
while (longitude < -LONGITUDE_MAX.intValue()) {
longitude = longitude + LONGITUDE_MAX.intValue() * 2;
}
while (longitude >= LONGITUDE_MAX.intValue()) {
longitude = longitude - LONGITUDE_MAX.intValue() * 2;
}
return longitude;
}
/**
* Compute the latitude precision value for a given code length. Lengths <= 10 have the same
* precision for latitude and longitude, but lengths > 10 have different precisions due to the
* grid method having fewer columns than rows. Copied from the JS implementation.
*/
private static double computeLatitudePrecision(int codeLength) {
if (codeLength <= CODE_PRECISION_NORMAL) {
return Math.pow(ENCODING_BASE.intValue(), Math.floor(codeLength / -2 + 2));
}
return Math.pow(ENCODING_BASE.intValue(), -3)
/ Math.pow(GRID_ROWS.intValue(), codeLength - PAIR_CODE_LENGTH);
}
}

View 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();
}
}

View file

@ -8,7 +8,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class TransportStop extends MapObject {
@ -111,6 +110,10 @@ public class TransportStop extends MapObject {
return !isDeleted() && referencesToRoutes != null && referencesToRoutes.length > 0;
}
public boolean hasReferencesToRoutesMap() {
return !isDeleted() && referencesToRoutesMap != null && !referencesToRoutesMap.isEmpty();
}
public Amenity getAmenity() {
if (transportStopAggregated != null) {
return transportStopAggregated.getAmenity();

5
OsmAnd/.gitignore vendored
View file

@ -13,6 +13,11 @@ libs/it.unibo.alice.tuprolog-tuprolog-3.2.1.jar
libs/commons-codec-commons-codec-1.11.jar
libs/OsmAndCore_android-0.1-SNAPSHOT.jar
libs/huawei-*.jar
huaweidrmlib/
HwDRM_SDK_*
drm_strings.xml
valgrind/
bin/
dist/

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<activity android:name="com.huawei.android.sdk.drm.DrmDialogActivity"
android:configChanges="screenSize|orientation|keyboardHidden"
android:exported="false"
android:theme="@android:style/Theme.Translucent">
<meta-data
android:name="hwc-theme"
android:value="androidhwext:style/Theme.Emui.Translucent" />
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="net.osmand.plus.huawei.fileprovider"
tools:replace="android:authorities" />
<service
android:name="net.osmand.plus.NavigationService"
android:process="net.osmand.plus.huawei"
tools:replace="android:process" />
</application>
</manifest>

View file

@ -17,7 +17,6 @@ apply plugin: 'com.android.application'
// </unzip>
// Less important
task printc {
configurations.each { if(it.isCanBeResolved()) println it.name }
}
@ -112,6 +111,9 @@ android {
freecustom {
manifest.srcFile "AndroidManifest-freecustom.xml"
}
huawei {
manifest.srcFile "AndroidManifest-huawei.xml"
}
legacy {
jniLibs.srcDirs = ["libc++"]
@ -175,6 +177,10 @@ android {
resConfig "en"
//resConfigs "xxhdpi", "nodpi"
}
huawei {
dimension "version"
applicationId "net.osmand.plus.huawei"
}
// CoreVersion
legacy {
@ -251,6 +257,46 @@ task downloadWorldMiniBasemap {
}
}
task downloadHuaweiDrmZip {
doLast {
ant.get(src: 'https://obs.cn-north-2.myhwclouds.com/hms-ds-wf/sdk/HwDRM_SDK_2.5.2.300_ADT.zip', dest: 'HwDRM_SDK_2.5.2.300_ADT.zip', skipexisting: 'true')
ant.unzip(src: 'HwDRM_SDK_2.5.2.300_ADT.zip', dest: 'huaweidrmlib/')
}
}
task copyHuaweiDrmLibs(type: Copy) {
dependsOn downloadHuaweiDrmZip
from "huaweidrmlib/HwDRM_SDK_2.5.2.300_ADT/libs"
into "libs"
}
task copyHuaweiDrmValues(type: Copy) {
dependsOn downloadHuaweiDrmZip
from "huaweidrmlib/HwDRM_SDK_2.5.2.300_ADT/res"
into "res"
}
task downloadPrebuiltHuaweiDrm {
dependsOn copyHuaweiDrmLibs, copyHuaweiDrmValues
}
task cleanHuaweiDrmLibs(type: Delete) {
delete "huaweidrmlib"
delete fileTree("libs").matching {
include "**/huawei-*.jar"
}
}
task cleanHuaweiDrmValues(type: Delete) {
delete fileTree("res").matching {
include "**/drm_strings.xml"
}
}
task cleanPrebuiltHuaweiDrm {
dependsOn cleanHuaweiDrmLibs, cleanHuaweiDrmValues
}
task collectVoiceAssets(type: Sync) {
from "../../resources/voice"
into "assets/voice"
@ -317,6 +363,15 @@ task collectExternalResources {
validateTranslate,
copyWidgetIcons,
downloadWorldMiniBasemap
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString().toLowerCase()
// Use Drm SDK only for huawei build
if (tskReqStr.contains("huawei")) {
dependsOn downloadPrebuiltHuaweiDrm
} else {
dependsOn cleanPrebuiltHuaweiDrm
}
}
// Legacy core build
@ -363,9 +418,9 @@ task cleanupDuplicatesInCore() {
}
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.javaCompiler.dependsOn(collectExternalResources, buildOsmAndCore, cleanupDuplicatesInCore)
}
android.applicationVariants.all { variant ->
variant.javaCompiler.dependsOn(collectExternalResources, buildOsmAndCore, cleanupDuplicatesInCore)
}
}
task appStart(type: Exec) {
@ -394,6 +449,8 @@ dependencies {
implementation 'com.moparisthebest:junidecode:0.1.1'
implementation 'org.immutables:gson:2.5.0'
implementation 'com.vividsolutions:jts-core:1.14.0'
implementation 'com.google.openlocationcode:openlocationcode:1.0.4'
implementation 'com.android.billingclient:billing:2.0.3'
// turn off for now
//implementation 'com.atilika.kuromoji:kuromoji-ipadic:0.9.0'
implementation 'com.squareup.picasso:picasso:2.71828'
@ -419,4 +476,6 @@ dependencies {
implementation ("com.github.HITGIF:TextFieldBoxes:1.3.5"){
exclude group: 'com.android.support'
}
huaweiImplementation files('libs/huawei-android-drm_v2.5.2.300.jar')
}

View file

@ -29,7 +29,7 @@
<string name="osm_live_3_months_price">€3,99</string>
<string name="osm_live_3_months_monthly_price">€1,33</string>
<string name="osm_live_annual_price">€7,99</string>
<string name="osm_live_annual_monthly_price">2,66</string>
<string name="osm_live_annual_monthly_price">0,66</string>
<string name="twitter_address">https://twitter.com/osmandapp</string>
<string name="reddit_address">https://www.reddit.com/r/OsmAnd</string>
<string name="facebook_address">https://www.facebook.com/osmandapp</string>

View file

@ -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">
<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
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">
<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
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_weight="1"
android:orientation="vertical">
<net.osmand.plus.widgets.TextViewEx
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="?attr/dialog_title_color"
android:textSize="@dimen/default_list_text_size"
osmand:typeface="@string/font_roboto_regular"
tools:text="Pay monthly" />
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_weight="1"
android:orientation="vertical">
<net.osmand.plus.widgets.TextViewEx
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="?attr/dialog_title_color"
android:textSize="@dimen/default_list_text_size"
osmand:typeface="@string/font_roboto_regular"
tools:text="Pay monthly" />
<net.osmand.plus.widgets.TextViewEx
android:id="@+id/description_contribute"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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
android:id="@+id/description"
@ -64,11 +63,20 @@
</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/img_feature_purchased" />
</LinearLayout>
<LinearLayout
android:id="@+id/button_view"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/wikivoyage_secondary_btn_bg">
@ -76,7 +84,7 @@
<LinearLayout
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_button_height"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="@dimen/list_header_padding">
@ -100,7 +108,7 @@
<LinearLayout
android:id="@+id/button_cancel_view"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/wikivoyage_primary_btn_bg">
@ -108,7 +116,8 @@
<LinearLayout
android:id="@+id/button_cancel"
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:orientation="horizontal"
android:padding="@dimen/list_header_padding">
@ -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" />
<View
android:id="@+id/div_bottom"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/subscription_active_div_color"
android:layout_marginTop="@dimen/content_padding_small"
android:background="?attr/wikivoyage_card_divider_color"
android:visibility="gone" />
</LinearLayout>

View file

@ -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">
<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
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_weight="1"
android:orientation="vertical">
<net.osmand.plus.widgets.TextViewEx
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="?attr/dialog_title_color"
android:textSize="@dimen/default_list_text_size"
osmand:typeface="@string/font_roboto_regular"
tools:text="Pay monthly" />
<LinearLayout
android:layout_width="wrap_content"
<net.osmand.plus.widgets.TextViewEx
android:id="@+id/description_contribute"
android:layout_width="match_parent"
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
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="?attr/card_description_text_color"
android:textSize="@dimen/default_desc_text_size"
osmand:typeface="@string/font_roboto_regular"
tools:text="Monthly payment" />
<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>
<net.osmand.plus.widgets.TextViewEx
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/card_description_text_color"
android:textSize="@dimen/default_desc_text_size"
osmand:typeface="@string/font_roboto_regular"
tools:text="$0.62 / month • Save 68%" />
</LinearLayout>
<LinearLayout
android:id="@+id/button_view"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/btn_round_border_2">
@ -95,7 +65,7 @@
<LinearLayout
android:id="@+id/button"
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:orientation="horizontal"
@ -119,7 +89,7 @@
android:textColor="?attr/color_dialog_buttons"
android:textSize="@dimen/text_button_text_size"
osmand:typeface="@string/font_roboto_medium"
tools:text="7,99€" />
tools:text="7,99€ / year" />
</LinearLayout>
@ -127,7 +97,7 @@
<LinearLayout
android:id="@+id/button_ex_view"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/wikivoyage_primary_btn_bg">
@ -135,7 +105,7 @@
<LinearLayout
android:id="@+id/button_ex"
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:orientation="horizontal"
@ -152,7 +122,7 @@
android:textColor="@color/color_white"
android:textSize="@dimen/text_button_text_size"
osmand:typeface="@string/font_roboto_medium"
tools:text="7,99€" />
tools:text="$3.99 for six month\nthan $7.49 / year" />
</LinearLayout>
@ -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" />

View file

@ -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" />

View file

@ -1,5 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<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_ice_road_name">Ледовый автозимник</string>
<string name="routeInfo_winter_ice_road_name">Зимники</string>

View file

@ -11,6 +11,30 @@
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="app_mode_camper">Camper</string>
<string name="app_mode_campervan">Campervan</string>
@ -1276,6 +1300,7 @@
<string name="lang_th">Thai</string>
<string name="lang_te">Telugu</string>
<string name="lang_nn">Norwegian Nynorsk</string>
<string name="lang_oc">Occitan</string>
<string name="lang_new">Newar / Nepal Bhasa</string>
<string name="lang_ms">Malaysian</string>
<string name="lang_ht">Haitian</string>

View file

@ -1,144 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import android.os.Bundle;
/**
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
* This service provides the following features:
* 1. Provides a new API to get details of in-app items published for the app including
* price, type, title and description.
* 2. The purchase flow is synchronous and purchase information is available immediately
* after it completes.
* 3. Purchase information of in-app purchases is maintained within the Google Play system
* till the purchase is consumed.
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
* in-app items are consumable and thereafter can be purchased again.
* 5. An API to get current purchases of the user immediately. This will not contain any
* consumed purchases.
*
* All calls will give a response code with the following possible values
* RESULT_OK = 0 - success
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
* RESULT_ERROR = 6 - Fatal error during the API action
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
*/
interface IInAppBillingService {
/**
* Checks support for the requested billing API version, package and in-app type.
* Minimum API version supported by this interface is 3.
* @param apiVersion the billing version which the app is using
* @param packageName the package name of the calling app
* @param type type of the in-app item being purchased "inapp" for one-time purchases
* and "subs" for subscription.
* @return RESULT_OK(0) on success, corresponding result code on failures
*/
int isBillingSupported(int apiVersion, String packageName, String type);
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
* with a list JSON strings containing the productId, price, title and description.
* This API can be called with a maximum of 20 SKUs.
* @param apiVersion billing API version that the Third-party is using
* @param packageName the package name of the calling app
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
* "title : "Example Title", "description" : "This is an example description" }'
*/
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
/**
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
* the type, a unique purchase token and an optional developer payload.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type the type of the in-app item ("inapp" for one-time purchases
* and "subs" for subscription).
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
* TODO: change this to app-specific keys.
*/
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
String developerPayload);
/**
* Returns the current SKUs owned by the user of the type and package name specified along with
* purchase information and a signature of the data to be validated.
* This will return all SKUs that have been purchased in V3 and managed items purchased using
* V1 and V2 that have not been consumed.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param type the type of the in-app items being requested
* ("inapp" for one-time purchases and "subs" for subscription).
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
/**
* Consume the last purchase of the given SKU. This will result in this item being removed
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return 0 if consumption succeeded. Appropriate error values for failures.
*/
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
}

View file

@ -30,6 +30,9 @@ 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;
import android.text.SpannableStringBuilder;
@ -60,6 +63,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;
@ -576,4 +580,8 @@ public class AndroidUtils {
return baseString;
}
}
public static boolean isRTL() {
return TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL;
}
}

View file

@ -5,11 +5,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.openlocationcode.OpenLocationCode;
import com.jwetherell.openmap.common.LatLonPoint;
import com.jwetherell.openmap.common.UTMPoint;
import java.util.LinkedHashMap;
import java.util.Map;
import net.osmand.LocationConvert;
import net.osmand.plus.OsmAndFormatter;
import net.osmand.plus.OsmandApplication;
@ -18,6 +14,9 @@ import net.osmand.plus.R;
import net.osmand.plus.activities.MapActivity;
import net.osmand.util.Algorithms;
import java.util.LinkedHashMap;
import java.util.Map;
public class PointDescription {
private String type = "";
private String name = "";

View file

@ -0,0 +1,66 @@
package net.osmand.plus;
import android.app.Activity;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class HuaweiDrmHelper {
private static final String TAG = HuaweiDrmHelper.class.getSimpleName();
//Copyright protection id
private static final String DRM_ID = "101117397";
//Copyright protection public key
private static final String DRM_PUBLIC_KEY = "9d6f861e7d46be167809a6a62302749a6753b3c1bd02c9729efb3973e268091d";
public static void check(Activity activity) {
boolean succeed = false;
try {
final WeakReference<Activity> activityRef = new WeakReference<>(activity);
Class<?> drmCheckCallbackClass = Class.forName("com.huawei.android.sdk.drm.DrmCheckCallback");
Object callback = java.lang.reflect.Proxy.newProxyInstance(
drmCheckCallbackClass.getClassLoader(),
new java.lang.Class[]{drmCheckCallbackClass},
new java.lang.reflect.InvocationHandler() {
@Override
public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) {
Activity a = activityRef.get();
if (a != null && !a.isFinishing()) {
String method_name = method.getName();
if (method_name.equals("onCheckSuccess")) {
// skip now
} else if (method_name.equals("onCheckFailed")) {
closeApplication(a);
}
}
return null;
}
});
Class<?> drmClass = Class.forName("com.huawei.android.sdk.drm.Drm");
Class[] partypes = new Class[]{Activity.class, String.class, String.class, String.class, drmCheckCallbackClass};
Method check = drmClass.getMethod("check", partypes);
check.invoke(null, activity, activity.getPackageName(), DRM_ID, DRM_PUBLIC_KEY, callback);
succeed = true;
} catch (ClassNotFoundException e) {
Log.e(TAG, "check: ", e);
} catch (NoSuchMethodException e) {
Log.e(TAG, "check: ", e);
} catch (IllegalAccessException e) {
Log.e(TAG, "check: ", e);
} catch (InvocationTargetException e) {
Log.e(TAG, "check: ", e);
}
if (!succeed) {
closeApplication(activity);
}
}
private static void closeApplication(Activity activity) {
((OsmandApplication) activity.getApplication()).closeApplicationAnywayImpl(activity, true);
}
}

View file

@ -286,7 +286,7 @@ public class NavigationService extends Service implements LocationListener {
@Override
public void onTaskRemoved(Intent rootIntent) {
OsmandApplication app = ((OsmandApplication) getApplication());
app.getNotificationHelper().removeNotifications();
app.getNotificationHelper().removeNotifications(false);
if (app.getNavigationService() != null &&
app.getSettings().DISABLE_RECORDING_ONCE_APP_KILLED.get()) {
NavigationService.this.stopSelf();

View file

@ -155,9 +155,11 @@ public class NotificationHelper {
}
}
public void removeNotifications() {
public void removeNotifications(boolean inactiveOnly) {
for (OsmandNotification notification : all) {
notification.removeNotification();
if (!inactiveOnly || !notification.isActive()) {
notification.removeNotification();
}
}
}

View file

@ -761,6 +761,9 @@ public class OsmAndLocationProvider implements SensorEventListener {
public void setLocationFromService(net.osmand.Location location, boolean continuous) {
if (locationSimulation.isRouteAnimating()) {
return;
}
// if continuous notify about lost location
if (continuous) {
scheduleCheckIfGpsLost(location);

View file

@ -242,7 +242,7 @@ public class OsmandApplication extends MultiDexApplication {
if(RateUsBottomSheetDialog.shouldShow(this)) {
osmandSettings.RATE_US_STATE.set(RateUsBottomSheetDialog.RateUsState.IGNORED);
}
getNotificationHelper().removeNotifications();
getNotificationHelper().removeNotifications(false);
}
public RendererRegistry getRendererRegistry() {

View file

@ -25,6 +25,10 @@ public class Version {
return ctx.getString(R.string.versionFeatures).contains("+blackberry");
}
public static boolean isHuawei(OsmandApplication ctx) {
return ctx.getPackageName().endsWith(".huawei");
}
public static boolean isMarketEnabled(OsmandApplication ctx) {
return isGooglePlayEnabled(ctx) || isAmazonEnabled(ctx);
}

View file

@ -64,6 +64,7 @@ import net.osmand.plus.AppInitializer.AppInitializeListener;
import net.osmand.plus.AppInitializer.InitEvents;
import net.osmand.plus.ApplicationMode;
import net.osmand.plus.GpxSelectionHelper.GpxDisplayItem;
import net.osmand.plus.HuaweiDrmHelper;
import net.osmand.plus.MapMarkersHelper;
import net.osmand.plus.MapMarkersHelper.MapMarker;
import net.osmand.plus.MapMarkersHelper.MapMarkerChangedListener;
@ -244,6 +245,10 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven
trackDetailsMenu.setMapActivity(this);
super.onCreate(savedInstanceState);
if (Version.isHuawei(getMyApplication())) {
HuaweiDrmHelper.check(this);
}
// Full screen is not used here
// getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.main);
@ -909,6 +914,13 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven
}
}
public void setKeepScreenOn(boolean keepScreenOn) {
View mainView = findViewById(R.id.MapViewWithLayers);
if (mainView != null) {
mainView.setKeepScreenOn(keepScreenOn);
}
}
private void clearIntent(Intent intent) {
intent.setAction(null);
intent.setData(null);
@ -1308,7 +1320,7 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven
@Override
protected void onStop() {
getMyApplication().getNotificationHelper().removeNotifications();
getMyApplication().getNotificationHelper().removeNotifications(true);
if(pendingPause) {
onPauseActivity();
}
@ -1397,6 +1409,7 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven
}
public void updateApplicationModeSettings() {
changeKeyguardFlags();
updateMapSettings();
mapViewTrackingUtilities.updateSettings();
//app.getRoutingHelper().setAppMode(settings.getApplicationMode());
@ -1894,23 +1907,29 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven
return oldPoint.getLayerId().equals(layerId) && oldPoint.getId().equals(point.getId());
}
public void changeKeyguardFlags(final boolean enable) {
public void changeKeyguardFlags() {
changeKeyguardFlags(settings.TURN_SCREEN_ON_TIME_INT.get() > 0, true);
}
private void changeKeyguardFlags(boolean enable, boolean forceKeepScreenOn) {
if (enable) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED,
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
setKeepScreenOn(true);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
setKeepScreenOn(forceKeepScreenOn);
}
}
@Override
public void lock() {
changeKeyguardFlags(false);
changeKeyguardFlags(false, false);
}
@Override
public void unlock() {
changeKeyguardFlags(true);
changeKeyguardFlags(true, false);
}
private class ScreenOffReceiver extends BroadcastReceiver {
@ -1976,12 +1995,14 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven
@Override
public void routeWasCancelled() {
changeKeyguardFlags();
}
@Override
public void routeWasFinished() {
if (!mIsDestroyed) {
DestinationReachedMenu.show(this);
changeKeyguardFlags();
}
}

View file

@ -54,17 +54,6 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In
deinitInAppPurchaseHelper();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Pass on the activity result to the helper for handling
if (purchaseHelper == null || !purchaseHelper.onActivityResultHandled(requestCode, resultCode, data)) {
// not handled, so handle it ourselves (here's where you'd
// perform any handling of activity results not related to in-app
// billing...
super.onActivityResult(requestCode, resultCode, data);
}
}
private void initInAppPurchaseHelper() {
deinitInAppPurchaseHelper();

View file

@ -277,6 +277,7 @@ public class SettingsGeneralActivity extends SettingsBaseActivity implements OnR
"nb",
"nl",
"nn",
"oc",
"pl",
"pt",
"pt_BR",
@ -337,6 +338,7 @@ public class SettingsGeneralActivity extends SettingsBaseActivity implements OnR
getString(R.string.lang_nb),
getString(R.string.lang_nl),
getString(R.string.lang_nn) + incompleteSuffix,
getString(R.string.lang_oc) + incompleteSuffix,
getString(R.string.lang_pl),
getString(R.string.lang_pt),
getString(R.string.lang_pt_br),

View file

@ -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,41 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment
} else if (osmLiveCardButtonsContainer != null) {
osmLiveCardButtonsContainer.removeAllViews();
View lastBtn = null;
InAppSubscription monthlyLiveUpdates = purchaseHelper.getMonthlyLiveUpdates();
double regularMonthlyPrice = monthlyLiveUpdates.getPriceValue();
List<InAppSubscription> 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 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 = (AppCompatImageView) buttonPurchased.findViewById(R.id.right_image);
CharSequence priceTitle = hasIntroductoryInfo ?
introductoryInfo.getFormattedDescription(ctx, buttonTitle.getCurrentTextColor()) : s.getPrice(ctx);
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,89 +391,52 @@ 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 = (AppCompatImageView) buttonCancel.findViewById(R.id.right_image);
title.setText(getString(R.string.osm_live_payment_current_subscription));
description.setText(s.getRenewDescription(ctx));
buttonView.setVisibility(View.GONE);
buttonCancelView.setVisibility(View.VISIBLE);
buttonCancel.setOnClickListener(new OnClickListener() {
buttonCancelView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
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);
TextViewEx discountRegular = (TextViewEx) button.findViewById(R.id.discount_banner_regular);
TextViewEx discountActive = (TextViewEx) button.findViewById(R.id.discount_banner_active);
boolean showSolidButton = !anyPurchasedOrIntroducted || hasIntroductoryInfo;
buttonView.setVisibility(!showSolidButton ? View.VISIBLE : View.GONE);
buttonExView.setVisibility(showSolidButton ? View.VISIBLE : View.GONE);
View div = button.findViewById(R.id.div);
CharSequence priceTitle = hasIntroductoryInfo ?
introductoryInfo.getFormattedDescription(ctx, buttonExTitle.getCurrentTextColor()) : s.getPrice(ctx);
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 +461,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);

View file

@ -828,7 +828,7 @@ public class ConfigureMapMenu {
}
}
public static String[] mapNamesIds = new String[]{"", "en", "af", "als", "ar", "az", "be", "ber", "bg", "bn", "bpy", "br", "bs", "ca", "ceb", "cs", "cy", "da", "de", "el", "eo", "es", "et", "eu", "fa", "fi", "fr", "fy", "ga", "gl", "he", "hi", "hsb", "hr", "ht", "hu", "hy", "id", "is", "it", "ja", "ka", "kab", "ko", "ku", "la", "lb", "lo", "lt", "lv", "mk", "ml", "mr", "ms", "nds", "new", "nl", "nn", "no", "nv", "os", "pl", "pms", "pt", "ro", "ru", "sc", "sh", "sk", "sl", "sq", "sr", "sv", "sw", "ta", "te", "th", "tl", "tr", "uk", "vi", "vo", "zh"};
public static String[] mapNamesIds = new String[]{"", "en", "af", "als", "ar", "az", "be", "ber", "bg", "bn", "bpy", "br", "bs", "ca", "ceb", "cs", "cy", "da", "de", "el", "eo", "es", "et", "eu", "fa", "fi", "fr", "fy", "ga", "gl", "he", "hi", "hsb", "hr", "ht", "hu", "hy", "id", "is", "it", "ja", "ka", "kab", "ko", "ku", "la", "lb", "lo", "lt", "lv", "mk", "ml", "mr", "ms", "nds", "new", "nl", "nn", "no", "nv", "oc", "os", "pl", "pms", "pt", "ro", "ru", "sc", "sh", "sk", "sl", "sq", "sr", "sv", "sw", "ta", "te", "th", "tl", "tr", "uk", "vi", "vo", "zh"};
public static String[] getSortedMapNamesIds(Context ctx, String[] ids, String[] values) {
final Map<String, String> mp = new HashMap<>();

View file

@ -1,5 +1,6 @@
package net.osmand.plus.helpers;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.hardware.Sensor;
@ -31,7 +32,6 @@ public class LockHelper implements SensorEventListener {
private CommonPreference<Integer> turnScreenOnTime;
private CommonPreference<Boolean> turnScreenOnSensor;
@Nullable
private LockUIAdapter lockUIAdapter;
private Runnable lockRunnable;
@ -75,35 +75,42 @@ public class LockHelper implements SensorEventListener {
}
}
private void unlock(long timeInMills) {
releaseWakeLocks();
@SuppressLint("WakelockTimeout")
private void unlock() {
if (lockUIAdapter != null) {
lockUIAdapter.unlock();
}
PowerManager pm = (PowerManager) app.getSystemService(Context.POWER_SERVICE);
if (pm != null) {
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK
| PowerManager.ACQUIRE_CAUSES_WAKEUP, "tso:wakelocktag");
wakeLock.acquire(timeInMills);
| PowerManager.ACQUIRE_CAUSES_WAKEUP, "OsmAnd:OnVoiceWakeupTag");
wakeLock.acquire();
}
}
private void lock() {
releaseWakeLocks();
if (lockUIAdapter != null) {
if (lockUIAdapter != null && isFollowingMode()) {
lockUIAdapter.lock();
}
}
private boolean isFollowingMode() {
return app.getRoutingHelper().isFollowingMode();
}
private void timedUnlock(final long millis) {
uiHandler.removeCallbacks(lockRunnable);
uiHandler.post(new Runnable() {
@Override
public void run() {
unlock(millis);
}
});
if (wakeLock == null) {
uiHandler.post(new Runnable() {
@Override
public void run() {
if (wakeLock == null) {
unlock();
}
}
});
}
uiHandler.postDelayed(lockRunnable, millis);
}
@ -149,16 +156,15 @@ public class LockHelper implements SensorEventListener {
}
private boolean isSensorEnabled() {
return turnScreenOnSensor.get() && app.getRoutingHelper().isFollowingMode();
return turnScreenOnSensor.get() && isFollowingMode();
}
public void onStart(@NonNull Activity activity) {
if (wakeLock == null) {
switchSensorOff();
}
switchSensorOff();
}
public void onStop(@NonNull Activity activity) {
lock();
if (!activity.isFinishing() && isSensorEnabled()) {
switchSensorOn();
}

View file

@ -2,15 +2,22 @@ package net.osmand.plus.inapp;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.android.billingclient.api.BillingClient.BillingResponseCode;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsResponseListener;
import net.osmand.AndroidNetworkUtils;
import net.osmand.AndroidNetworkUtils.OnRequestResultListener;
import net.osmand.PlatformUtil;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.OsmandSettings;
import net.osmand.plus.OsmandSettings.OsmandPreference;
@ -20,14 +27,10 @@ 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.IabHelper;
import net.osmand.plus.inapp.util.IabHelper.OnIabPurchaseFinishedListener;
import net.osmand.plus.inapp.util.IabHelper.QueryInventoryFinishedListener;
import net.osmand.plus.inapp.util.IabResult;
import net.osmand.plus.inapp.util.Inventory;
import net.osmand.plus.inapp.util.Purchase;
import net.osmand.plus.inapp.util.SkuDetails;
import net.osmand.plus.inapp.util.BillingManager;
import net.osmand.plus.inapp.util.BillingManager.BillingUpdatesListener;
import net.osmand.plus.liveupdates.CountrySelectionFragment;
import net.osmand.plus.liveupdates.CountrySelectionFragment.CountryItem;
import net.osmand.util.Algorithms;
@ -37,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;
@ -46,13 +50,11 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static net.osmand.plus.inapp.util.IabHelper.IABHELPER_USER_CANCELLED;
import static net.osmand.plus.inapp.util.IabHelper.ITEM_TYPE_SUBS;
public class InAppPurchaseHelper {
// Debug tag, for logging
private static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(InAppPurchaseHelper.class);
private static final String TAG = InAppPurchaseHelper.class.getSimpleName();
private boolean mDebugLog = true;
private boolean mDebugLog = false;
public static final long SUBSCRIPTION_HOLDING_TIME_MSEC = 1000 * 60 * 60 * 24 * 3; // 3 days
@ -64,7 +66,9 @@ public class InAppPurchaseHelper {
private static final int RC_REQUEST = 10001;
// The helper object
private IabHelper mHelper;
private BillingManager billingManager;
private List<SkuDetails> skuDetailsList;
private boolean isDeveloperVersion;
private String token = "";
private InAppPurchaseTaskType activeTask;
@ -186,6 +190,10 @@ public class InAppPurchaseHelper {
return false;
}
private BillingManager getBillingManager() {
return billingManager;
}
private void exec(final @NonNull InAppPurchaseTaskType taskType, final @NonNull InAppRunnable runnable) {
if (isDeveloperVersion || !Version.isGooglePlayEnabled(ctx)) {
return;
@ -201,10 +209,6 @@ public class InAppPurchaseHelper {
// Create the helper, passing it our context and the public key to verify signatures with
logDebug("Creating InAppPurchaseHelper.");
mHelper = new IabHelper(ctx, BASE64_ENCODED_PUBLIC_KEY);
// enable debug logging (for a production application, you should set this to false).
mHelper.enableDebugLogging(false);
// Start setup. This is asynchronous and the specified listener
// will be called once setup completes.
@ -212,26 +216,110 @@ public class InAppPurchaseHelper {
try {
processingTask = true;
activeTask = taskType;
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingUpdatesListener() {
@Override
public void onBillingClientSetupFinished() {
logDebug("Setup finished.");
if (!result.isSuccess()) {
// Oh noes, there was a problem.
//complain("Problem setting up in-app billing: " + result);
notifyError(taskType, result.getMessage());
BillingManager billingManager = getBillingManager();
// Have we been disposed of in the meantime? If so, quit.
if (billingManager == null) {
stop(true);
return;
}
// Have we been disposed of in the meantime? If so, quit.
if (mHelper == null) {
if (!billingManager.isServiceConnected()) {
// Oh noes, there was a problem.
//complain("Problem setting up in-app billing: " + result);
notifyError(taskType, billingManager.getBillingClientResponseMessage());
stop(true);
return;
}
processingTask = !runnable.run(InAppPurchaseHelper.this);
}
@Override
public void onConsumeFinished(String token, BillingResult billingResult) {
}
@Override
public void onPurchasesUpdated(final List<Purchase> purchases) {
BillingManager billingManager = getBillingManager();
// Have we been disposed of in the meantime? If so, quit.
if (billingManager == null) {
stop(true);
return;
}
if (activeTask == InAppPurchaseTaskType.REQUEST_INVENTORY) {
List<String> skuInApps = new ArrayList<>();
for (InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) {
skuInApps.add(purchase.getSku());
}
for (Purchase p : purchases) {
skuInApps.add(p.getSku());
}
billingManager.querySkuDetailsAsync(SkuType.INAPP, skuInApps, new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListInApps) {
// Is it a failure?
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
logError("Failed to query inapps sku details: " + billingResult.getResponseCode());
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
stop(true);
return;
}
List<String> skuSubscriptions = new ArrayList<>();
for (InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) {
skuSubscriptions.add(subscription.getSku());
}
for (Purchase p : purchases) {
skuSubscriptions.add(p.getSku());
}
BillingManager billingManager = getBillingManager();
// Have we been disposed of in the meantime? If so, quit.
if (billingManager == null) {
stop(true);
return;
}
billingManager.querySkuDetailsAsync(SkuType.SUBS, skuSubscriptions, new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListSubscriptions) {
// Is it a failure?
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
logError("Failed to query subscriptipons sku details: " + billingResult.getResponseCode());
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
stop(true);
return;
}
List<SkuDetails> skuDetailsList = new ArrayList<>(skuDetailsListInApps);
skuDetailsList.addAll(skuDetailsListSubscriptions);
InAppPurchaseHelper.this.skuDetailsList = skuDetailsList;
mSkuDetailsResponseListener.onSkuDetailsResponse(billingResult, skuDetailsList);
}
});
}
});
}
for (Purchase purchase : purchases) {
if (!purchase.isAcknowledged()) {
onPurchaseFinished(purchase);
}
}
}
@Override
public void onPurchaseCanceled() {
stop(true);
}
});
} catch (Exception e) {
logError("exec Error", e);
@ -255,8 +343,16 @@ public class InAppPurchaseHelper {
@Override
public boolean run(InAppPurchaseHelper helper) {
try {
mHelper.launchPurchaseFlow(activity,
getFullVersion().getSku(), RC_REQUEST, mPurchaseFinishedListener);
SkuDetails skuDetails = getSkuDetails(getFullVersion().getSku());
if (skuDetails == null) {
throw new IllegalArgumentException("Cannot find sku details");
}
BillingManager billingManager = getBillingManager();
if (billingManager != null) {
billingManager.initiatePurchaseFlow(activity, skuDetails);
} else {
throw new IllegalStateException("BillingManager disposed");
}
return false;
} catch (Exception e) {
complain("Cannot launch full version purchase!");
@ -281,8 +377,16 @@ public class InAppPurchaseHelper {
@Override
public boolean run(InAppPurchaseHelper helper) {
try {
mHelper.launchPurchaseFlow(activity,
getDepthContours().getSku(), RC_REQUEST, mPurchaseFinishedListener);
SkuDetails skuDetails = getSkuDetails(getDepthContours().getSku());
if (skuDetails == null) {
throw new IllegalArgumentException("Cannot find sku details");
}
BillingManager billingManager = getBillingManager();
if (billingManager != null) {
billingManager.initiatePurchaseFlow(activity, skuDetails);
} else {
throw new IllegalStateException("BillingManager disposed");
}
return false;
} catch (Exception e) {
complain("Cannot launch depth contours purchase!");
@ -294,26 +398,76 @@ public class InAppPurchaseHelper {
});
}
@Nullable
private SkuDetails getSkuDetails(@NonNull String sku) {
List<SkuDetails> skuDetailsList = this.skuDetailsList;
if (skuDetailsList != null) {
for (SkuDetails details : skuDetailsList) {
if (details.getSku().equals(sku)) {
return details;
}
}
}
return null;
}
private boolean hasDetails(@NonNull String sku) {
return getSkuDetails(sku) != null;
}
@Nullable
private Purchase getPurchase(@NonNull String sku) {
BillingManager billingManager = getBillingManager();
if (billingManager != null) {
List<Purchase> purchases = billingManager.getPurchases();
if (purchases != null) {
for (Purchase p : purchases) {
if (p.getSku().equals(sku)) {
return p;
}
}
}
}
return null;
}
// Listener that's called when we finish querying the items and subscriptions we own
private QueryInventoryFinishedListener mGotInventoryListener = new QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
logDebug("Query inventory finished.");
private SkuDetailsResponseListener mSkuDetailsResponseListener = new SkuDetailsResponseListener() {
@NonNull
private List<String> getAllOwnedSubscriptionSkus() {
List<String> result = new ArrayList<>();
BillingManager billingManager = getBillingManager();
if (billingManager != null) {
for (Purchase p : billingManager.getPurchases()) {
if (getInAppPurchases().getInAppSubscriptionBySku(p.getSku()) != null) {
result.add(p.getSku());
}
}
}
return result;
}
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
logDebug("Query sku details finished.");
// Have we been disposed of in the meantime? If so, quit.
if (mHelper == null) {
if (getBillingManager() == null) {
stop(true);
return;
}
// Is it a failure?
if (result.isFailure()) {
logError("Failed to query inventory: " + result);
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, result.getMessage());
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
logError("Failed to query inventory: " + billingResult.getResponseCode());
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
stop(true);
return;
}
logDebug("Query inventory was successful.");
logDebug("Query sku details was successful.");
/*
* Check for items we own. Notice that for each purchase, we check
@ -321,54 +475,64 @@ public class InAppPurchaseHelper {
* verifyDeveloperPayload().
*/
List<String> allOwnedSubscriptionSkus = inventory.getAllOwnedSkus(ITEM_TYPE_SUBS);
for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) {
if (inventory.hasDetails(p.getSku())) {
Purchase purchase = inventory.getPurchase(p.getSku());
SkuDetails liveUpdatesDetails = inventory.getSkuDetails(p.getSku());
fetchInAppPurchase(p, liveUpdatesDetails, purchase);
allOwnedSubscriptionSkus.remove(p.getSku());
List<String> allOwnedSubscriptionSkus = getAllOwnedSubscriptionSkus();
for (InAppSubscription s : getLiveUpdates().getAllSubscriptions()) {
if (hasDetails(s.getSku())) {
Purchase purchase = getPurchase(s.getSku());
SkuDetails liveUpdatesDetails = getSkuDetails(s.getSku());
if (liveUpdatesDetails != null) {
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
}
allOwnedSubscriptionSkus.remove(s.getSku());
}
}
for (String sku : allOwnedSubscriptionSkus) {
Purchase purchase = inventory.getPurchase(sku);
SkuDetails liveUpdatesDetails = inventory.getSkuDetails(sku);
InAppSubscription s = getLiveUpdates().upgradeSubscription(sku);
if (s == null) {
s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails);
Purchase purchase = getPurchase(sku);
SkuDetails liveUpdatesDetails = getSkuDetails(sku);
if (liveUpdatesDetails != null) {
InAppSubscription s = getLiveUpdates().upgradeSubscription(sku);
if (s == null) {
s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails);
}
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
}
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
}
InAppPurchase fullVersion = getFullVersion();
if (inventory.hasDetails(fullVersion.getSku())) {
Purchase purchase = inventory.getPurchase(fullVersion.getSku());
SkuDetails fullPriceDetails = inventory.getSkuDetails(fullVersion.getSku());
fetchInAppPurchase(fullVersion, fullPriceDetails, purchase);
if (hasDetails(fullVersion.getSku())) {
Purchase purchase = getPurchase(fullVersion.getSku());
SkuDetails fullPriceDetails = getSkuDetails(fullVersion.getSku());
if (fullPriceDetails != null) {
fetchInAppPurchase(fullVersion, fullPriceDetails, purchase);
}
}
InAppPurchase depthContours = getDepthContours();
if (inventory.hasDetails(depthContours.getSku())) {
Purchase purchase = inventory.getPurchase(depthContours.getSku());
SkuDetails depthContoursDetails = inventory.getSkuDetails(depthContours.getSku());
fetchInAppPurchase(depthContours, depthContoursDetails, purchase);
if (hasDetails(depthContours.getSku())) {
Purchase purchase = getPurchase(depthContours.getSku());
SkuDetails depthContoursDetails = getSkuDetails(depthContours.getSku());
if (depthContoursDetails != null) {
fetchInAppPurchase(depthContours, depthContoursDetails, purchase);
}
}
InAppPurchase contourLines = getContourLines();
if (inventory.hasDetails(contourLines.getSku())) {
Purchase purchase = inventory.getPurchase(contourLines.getSku());
SkuDetails contourLinesDetails = inventory.getSkuDetails(contourLines.getSku());
fetchInAppPurchase(contourLines, contourLinesDetails, purchase);
if (hasDetails(contourLines.getSku())) {
Purchase purchase = getPurchase(contourLines.getSku());
SkuDetails contourLinesDetails = getSkuDetails(contourLines.getSku());
if (contourLinesDetails != null) {
fetchInAppPurchase(contourLines, contourLinesDetails, purchase);
}
}
Purchase fullVersionPurchase = inventory.getPurchase(fullVersion.getSku());
boolean fullVersionPurchased = (fullVersionPurchase != null && fullVersionPurchase.getPurchaseState() == 0);
Purchase fullVersionPurchase = getPurchase(fullVersion.getSku());
boolean fullVersionPurchased = fullVersionPurchase != null;
if (fullVersionPurchased) {
ctx.getSettings().FULL_VERSION_PURCHASED.set(true);
}
Purchase depthContoursPurchase = inventory.getPurchase(depthContours.getSku());
boolean depthContoursPurchased = (depthContoursPurchase != null && depthContoursPurchase.getPurchaseState() == 0);
Purchase depthContoursPurchase = getPurchase(depthContours.getSku());
boolean depthContoursPurchased = depthContoursPurchase != null;
if (depthContoursPurchased) {
ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true);
}
@ -377,10 +541,10 @@ public class InAppPurchaseHelper {
boolean subscribedToLiveUpdates = false;
List<Purchase> liveUpdatesPurchases = new ArrayList<>();
for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) {
Purchase purchase = inventory.getPurchase(p.getSku());
Purchase purchase = getPurchase(p.getSku());
if (purchase != null) {
liveUpdatesPurchases.add(purchase);
if (!subscribedToLiveUpdates && purchase.getPurchaseState() == 0) {
if (!subscribedToLiveUpdates) {
subscribedToLiveUpdates = true;
}
}
@ -453,8 +617,7 @@ public class InAppPurchaseHelper {
private void fetchInAppPurchase(@NonNull InAppPurchase inAppPurchase, @NonNull SkuDetails skuDetails, @Nullable Purchase purchase) {
if (purchase != null) {
inAppPurchase.setPurchaseState(purchase.getPurchaseState() == 0
? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED);
inAppPurchase.setPurchaseState(PurchaseState.PURCHASED);
inAppPurchase.setPurchaseTime(purchase.getPurchaseTime());
} else {
inAppPurchase.setPurchaseState(PurchaseState.NOT_PURCHASED);
@ -467,7 +630,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);
}
}
}
}
@ -563,10 +745,15 @@ public class InAppPurchaseHelper {
public boolean run(InAppPurchaseHelper helper) {
try {
Activity a = activity.get();
if (a != null) {
mHelper.launchPurchaseFlow(a,
sku, ITEM_TYPE_SUBS,
RC_REQUEST, mPurchaseFinishedListener, payload);
SkuDetails skuDetails = getSkuDetails(sku);
if (a != null && skuDetails != null) {
BillingManager billingManager = getBillingManager();
if (billingManager != null) {
billingManager.setPayload(payload);
billingManager.initiatePurchaseFlow(a, skuDetails);
} else {
throw new IllegalStateException("BillingManager disposed");
}
return false;
} else {
stop(true);
@ -585,28 +772,6 @@ public class InAppPurchaseHelper {
}
}
public boolean onActivityResultHandled(int requestCode, int resultCode, Intent data) {
logDebug("onActivityResult(" + requestCode + "," + resultCode + "," + data);
if (mHelper == null) return false;
try {
// Pass on the activity result to the helper for handling
if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
// not handled, so handle it ourselves (here's where you'd
// perform any handling of activity results not related to in-app
// billing...
//super.onActivityResult(requestCode, resultCode, data);
return false;
} else {
logDebug("onActivityResult handled by IABUtil.");
return true;
}
} catch (Exception e) {
logError("onActivityResultHandled", e);
return false;
}
}
@SuppressLint("StaticFieldLeak")
private class RequestInventoryTask extends AsyncTask<Void, Void, String> {
@ -652,12 +817,13 @@ public class InAppPurchaseHelper {
@Override
public boolean run(InAppPurchaseHelper helper) {
logDebug("Setup successful. Querying inventory.");
Set<String> skus = new HashSet<>();
for (InAppPurchase purchase : purchases.getAllInAppPurchases()) {
skus.add(purchase.getSku());
}
try {
mHelper.queryInventoryAsync(true, new ArrayList<>(skus), mGotInventoryListener);
BillingManager billingManager = getBillingManager();
if (billingManager != null) {
billingManager.queryPurchases();
} else {
throw new IllegalStateException("BillingManager disposed");
}
return false;
} catch (Exception e) {
logError("queryInventoryAsync Error", e);
@ -679,81 +845,69 @@ public class InAppPurchaseHelper {
parameters.put("aid", ctx.getUserAndroidId());
}
// Callback for when a purchase is finished
private OnIabPurchaseFinishedListener mPurchaseFinishedListener = new OnIabPurchaseFinishedListener() {
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
logDebug("Purchase finished: " + result + ", purchase: " + purchase);
// Call when a purchase is finished
private void onPurchaseFinished(Purchase purchase) {
logDebug("Purchase finished: " + purchase);
// if we were disposed of in the meantime, quit.
if (mHelper == null) {
stop(true);
return;
}
if (result.isFailure()) {
if (result.getResponse() != IABHELPER_USER_CANCELLED) {
complain("Error purchasing: " + result);
}
notifyDismissProgress(activeTask);
notifyError(activeTask, "Error purchasing: " + result);
stop(true);
return;
}
logDebug("Purchase successful.");
InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(purchase.getSku());
if (liveUpdatesPurchase != null) {
// bought live updates
logDebug("Live updates subscription purchased.");
final String sku = liveUpdatesPurchase.getSku();
liveUpdatesPurchase.setPurchaseState(purchase.getPurchaseState() == 0 ? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED);
sendTokens(Collections.singletonList(purchase), new OnRequestResultListener() {
@Override
public void onResult(String result) {
boolean active = ctx.getSettings().LIVE_UPDATES_PURCHASED.get();
ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true);
ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(true);
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME.set(0L);
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false);
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false);
notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES);
notifyItemPurchased(sku, active);
stop(true);
}
});
} else if (purchase.getSku().equals(getFullVersion().getSku())) {
// bought full version
getFullVersion().setPurchaseState(purchase.getPurchaseState() == 0 ? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED);
logDebug("Full version purchased.");
showToast(ctx.getString(R.string.full_version_thanks));
ctx.getSettings().FULL_VERSION_PURCHASED.set(true);
notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION);
notifyItemPurchased(getFullVersion().getSku(), false);
stop(true);
} else if (purchase.getSku().equals(getDepthContours().getSku())) {
// bought sea depth contours
getDepthContours().setPurchaseState(purchase.getPurchaseState() == 0 ? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED);
logDebug("Sea depth contours purchased.");
showToast(ctx.getString(R.string.sea_depth_thanks));
ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true);
ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(true);
notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS);
notifyItemPurchased(getDepthContours().getSku(), false);
stop(true);
} else {
notifyDismissProgress(activeTask);
stop(true);
}
// if we were disposed of in the meantime, quit.
if (getBillingManager() == null) {
stop(true);
return;
}
};
logDebug("Purchase successful.");
InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(purchase.getSku());
if (liveUpdatesPurchase != null) {
// bought live updates
logDebug("Live updates subscription purchased.");
final String sku = liveUpdatesPurchase.getSku();
liveUpdatesPurchase.setPurchaseState(PurchaseState.PURCHASED);
sendTokens(Collections.singletonList(purchase), new OnRequestResultListener() {
@Override
public void onResult(String result) {
boolean active = ctx.getSettings().LIVE_UPDATES_PURCHASED.get();
ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true);
ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(true);
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME.set(0L);
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false);
ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false);
notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES);
notifyItemPurchased(sku, active);
stop(true);
}
});
} else if (purchase.getSku().equals(getFullVersion().getSku())) {
// bought full version
getFullVersion().setPurchaseState(PurchaseState.PURCHASED);
logDebug("Full version purchased.");
showToast(ctx.getString(R.string.full_version_thanks));
ctx.getSettings().FULL_VERSION_PURCHASED.set(true);
notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION);
notifyItemPurchased(getFullVersion().getSku(), false);
stop(true);
} else if (purchase.getSku().equals(getDepthContours().getSku())) {
// bought sea depth contours
getDepthContours().setPurchaseState(PurchaseState.PURCHASED);
logDebug("Sea depth contours purchased.");
showToast(ctx.getString(R.string.sea_depth_thanks));
ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true);
ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(true);
notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS);
notifyItemPurchased(getDepthContours().getSku(), false);
stop(true);
} else {
notifyDismissProgress(activeTask);
stop(true);
}
}
// Do not forget call stop() when helper is not needed anymore
public void stop() {
@ -762,14 +916,15 @@ public class InAppPurchaseHelper {
private void stop(boolean taskDone) {
logDebug("Destroying helper.");
if (mHelper != null) {
BillingManager billingManager = getBillingManager();
if (billingManager != null) {
if (taskDone) {
processingTask = false;
}
if (!processingTask) {
activeTask = null;
mHelper.dispose();
mHelper = null;
billingManager.destroy();
this.billingManager = null;
}
} else {
processingTask = false;
@ -793,7 +948,7 @@ public class InAppPurchaseHelper {
Map<String, String> parameters = new HashMap<>();
parameters.put("userid", userId);
parameters.put("sku", purchase.getSku());
parameters.put("purchaseToken", purchase.getToken());
parameters.put("purchaseToken", purchase.getPurchaseToken());
parameters.put("email", email);
parameters.put("token", token);
addUserInfo(parameters);

View file

@ -1,19 +1,29 @@
package net.osmand.plus.inapp;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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.inapp.util.SkuDetails;
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;
@ -122,15 +132,31 @@ public class InAppPurchases {
return liveUpdates;
}
public List<InAppPurchase> getAllInAppPurchases() {
public List<InAppPurchase> getAllInAppPurchases(boolean includeSubscriptions) {
List<InAppPurchase> purchases = new ArrayList<>();
purchases.add(fullVersion);
purchases.add(depthContours);
purchases.add(contourLines);
purchases.addAll(liveUpdates.getAllSubscriptions());
if (includeSubscriptions) {
purchases.addAll(liveUpdates.getAllSubscriptions());
}
return purchases;
}
public List<InAppSubscription> getAllInAppSubscriptions() {
return liveUpdates.getAllSubscriptions();
}
@Nullable
public InAppSubscription getInAppSubscriptionBySku(@NonNull String sku) {
for (InAppSubscription s : liveUpdates.getAllSubscriptions()) {
if (sku.startsWith(s.getSkuNoVersion())) {
return s;
}
}
return null;
}
public boolean isFullVersion(String sku) {
return FULL_VERSION.getSku().equals(sku);
}
@ -162,7 +188,7 @@ public class InAppPurchases {
private List<InAppSubscription> subscriptions;
InAppSubscriptionList(@NonNull InAppSubscription[] subscriptionsArray) {
this.subscriptions = Arrays.asList(subscriptionsArray);;
this.subscriptions = Arrays.asList(subscriptionsArray);
}
private List<InAppSubscription> getSubscriptions() {
@ -420,13 +446,205 @@ 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, @ColorInt int textColor) {
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 ForegroundColorSpan(textColor), 0, mainPart.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mainPart.setSpan(new CustomTypefaceSpan(typefaceBold), 0, mainPart.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
int secondaryTextColor = Color.argb(128, Color.red(textColor), Color.green(textColor), Color.blue(textColor));
thenPart.setSpan(new ForegroundColorSpan(secondaryTextColor), 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<String, InAppSubscription> 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;
@ -475,12 +693,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
@ -497,12 +739,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 {
@ -616,14 +880,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);

View file

@ -0,0 +1,420 @@
package net.osmand.plus.inapp.util;
import android.app.Activity;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClient.BillingResponseCode;
import com.android.billingclient.api.BillingClient.FeatureType;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchasesResult;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import net.osmand.PlatformUtil;
import net.osmand.util.Algorithms;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Handles all the interactions with Play Store (via Billing library), maintains connection to
* it through BillingClient and caches temporary states/data if needed
*/
public class BillingManager implements PurchasesUpdatedListener {
// Default value of mBillingClientResponseCode until BillingManager was not yeat initialized
public static final int BILLING_MANAGER_NOT_INITIALIZED = -1;
private static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(BillingManager.class);
private static final String TAG = "BillingManager";
/**
* A reference to BillingClient
**/
private BillingClient mBillingClient;
/**
* True if billing service is connected now.
*/
private boolean mIsServiceConnected;
private final Context mContext;
// Public key for verifying signature, in base64 encoding
private String mSignatureBase64;
private String mPayload;
private final BillingUpdatesListener mBillingUpdatesListener;
private final List<Purchase> mPurchases = new ArrayList<>();
private Set<String> mTokensToBeConsumed;
private int mBillingClientResponseCode = BILLING_MANAGER_NOT_INITIALIZED;
private String mBillingClientResponseMessage;
/**
* Listener to the updates that happen when purchases list was updated or consumption of the
* item was finished
*/
public interface BillingUpdatesListener {
void onBillingClientSetupFinished();
void onConsumeFinished(String token, BillingResult billingResult);
void onPurchasesUpdated(List<Purchase> purchases);
void onPurchaseCanceled();
}
/**
* Listener for the Billing client state to become connected
*/
public interface ServiceConnectedListener {
void onServiceConnected(BillingResult billingResult);
}
public BillingManager(@NonNull final Context context, @NonNull final String base64PublicKey,
@NonNull final BillingUpdatesListener updatesListener) {
LOG.debug("Creating Billing client.");
mContext = context;
mSignatureBase64 = base64PublicKey;
mBillingUpdatesListener = updatesListener;
mBillingClient = BillingClient.newBuilder(mContext)
.enablePendingPurchases()
.setListener(this)
.build();
LOG.debug("Starting setup.");
// Start setup. This is asynchronous and the specified listener will be called
// once setup completes.
// It also starts to report all the new purchases through onPurchasesUpdated() callback.
startServiceConnection(null);
}
/**
* Handle a callback that purchases were updated from the Billing library
*/
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
int responseCode = billingResult.getResponseCode();
if (responseCode == BillingResponseCode.OK) {
if (purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else {
LOG.info("onPurchasesUpdated() - no purchases");
}
mBillingUpdatesListener.onPurchasesUpdated(mPurchases);
} else if (responseCode == BillingResponseCode.USER_CANCELED) {
LOG.info("onPurchasesUpdated() - user cancelled the purchase flow - skipping");
mBillingUpdatesListener.onPurchaseCanceled();
} else {
LOG.warn("onPurchasesUpdated() got unknown responseCode: " + responseCode);
}
}
/**
* Start a purchase flow
*/
public void initiatePurchaseFlow(final Activity activity, final SkuDetails skuDetails) {
initiatePurchaseFlow(activity, skuDetails, null);
}
/**
* Start a purchase or subscription replace flow
*/
public void initiatePurchaseFlow(final Activity activity, final SkuDetails skuDetails, final String oldSku) {
Runnable purchaseFlowRequest = new Runnable() {
@Override
public void run() {
LOG.debug("Launching in-app purchase flow. Replace old SKU? " + (oldSku != null));
BillingFlowParams purchaseParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).setOldSku(oldSku).build();
mBillingClient.launchBillingFlow(activity, purchaseParams);
}
};
executeServiceRequest(purchaseFlowRequest);
}
public Context getContext() {
return mContext;
}
/**
* Clear the resources
*/
public void destroy() {
LOG.debug("Destroying the manager.");
if (mBillingClient != null && mBillingClient.isReady()) {
mBillingClient.endConnection();
mBillingClient = null;
}
}
public void querySkuDetailsAsync(@SkuType final String itemType, final List<String> skuList,
final SkuDetailsResponseListener listener) {
// Creating a runnable from the request to use it inside our connection retry policy below
Runnable queryRequest = new Runnable() {
@Override
public void run() {
// Query the purchase async
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(itemType);
mBillingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
listener.onSkuDetailsResponse(billingResult, skuDetailsList);
}
});
}
};
executeServiceRequest(queryRequest);
}
public void consumeAsync(final ConsumeParams consumeParams) {
// If we've already scheduled to consume this token - no action is needed (this could happen
// if you received the token when querying purchases inside onReceive() and later from
// onActivityResult()
final String purchaseToken = consumeParams.getPurchaseToken();
if (mTokensToBeConsumed == null) {
mTokensToBeConsumed = new HashSet<>();
} else if (mTokensToBeConsumed.contains(purchaseToken)) {
LOG.info("Token was already scheduled to be consumed - skipping...");
return;
}
mTokensToBeConsumed.add(purchaseToken);
// Generating Consume Response listener
final ConsumeResponseListener onConsumeListener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
// If billing service was disconnected, we try to reconnect 1 time
// (feel free to introduce your retry policy here).
mBillingUpdatesListener.onConsumeFinished(purchaseToken, billingResult);
}
};
// Creating a runnable from the request to use it inside our connection retry policy below
Runnable consumeRequest = new Runnable() {
@Override
public void run() {
// Consume the purchase async
mBillingClient.consumeAsync(consumeParams, onConsumeListener);
}
};
executeServiceRequest(consumeRequest);
}
public boolean isServiceConnected() {
return mIsServiceConnected;
}
/**
* Returns the value Billing client response code or BILLING_MANAGER_NOT_INITIALIZED if the
* client connection response was not received yet.
*/
public int getBillingClientResponseCode() {
return mBillingClientResponseCode;
}
public String getBillingClientResponseMessage() {
return mBillingClientResponseMessage;
}
public List<Purchase> getPurchases() {
return Collections.unmodifiableList(mPurchases);
}
public String getPayload() {
return mPayload;
}
public void setPayload(String payload) {
this.mPayload = payload;
}
/**
* Handles the purchase
* <p>Note: Notice that for each purchase, we check if signature is valid on the client.
* It's recommended to move this check into your backend.
* See {@link Security#verifyPurchase(String, String, String)}
* </p>
*
* @param purchase Purchase to be handled
*/
private void handlePurchase(final Purchase purchase) {
if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
LOG.info("Got a purchase: " + purchase + ", but signature is bad. Skipping...");
return;
}
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
// Acknowledge the purchase if it hasn't already been acknowledged.
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams.Builder builder =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken());
if (!Algorithms.isEmpty(mPayload)) {
builder.setDeveloperPayload(mPayload);
}
AcknowledgePurchaseParams acknowledgePurchaseParams = builder.build();
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
LOG.info("Acknowledge a purchase: " + purchase + " failed (" + billingResult.getResponseCode() + "). " + billingResult.getDebugMessage());
}
}
});
}
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
LOG.info("Got a purchase: " + purchase + ", but purchase state is pending. Skipping...");
return;
} else {
LOG.info("Got a purchase: " + purchase + ", but purchase state is " + purchase.getPurchaseState() + ". Skipping...");
return;
}
LOG.debug("Got a verified purchase: " + purchase);
mPurchases.add(purchase);
}
/**
* Handle a result from querying of purchases and report an updated list to the listener
*/
private void onQueryPurchasesFinished(PurchasesResult result) {
// Have we been disposed of in the meantime? If so, or bad result code, then quit
if (mBillingClient == null || result.getResponseCode() != BillingResponseCode.OK) {
LOG.warn("Billing client was null or result code (" + result.getResponseCode()
+ ") was bad - quitting");
return;
}
LOG.debug("Query inventory was successful.");
// Update the UI and purchases inventory with new list of purchases
mPurchases.clear();
onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList());
}
/**
* Checks if subscriptions are supported for current client
* <p>Note: This method does not automatically retry for RESULT_SERVICE_DISCONNECTED.
* It is only used in unit tests and after queryPurchases execution, which already has
* a retry-mechanism implemented.
* </p>
*/
public boolean areSubscriptionsSupported() {
int responseCode = mBillingClient.isFeatureSupported(FeatureType.SUBSCRIPTIONS).getResponseCode();
if (responseCode != BillingResponseCode.OK) {
LOG.warn("areSubscriptionsSupported() got an error response: " + responseCode);
}
return responseCode == BillingResponseCode.OK;
}
/**
* Query purchases across various use cases and deliver the result in a formalized way through
* a listener
*/
public void queryPurchases() {
Runnable queryToExecute = new Runnable() {
@Override
public void run() {
long time = System.currentTimeMillis();
PurchasesResult purchasesResult = mBillingClient.queryPurchases(SkuType.INAPP);
LOG.info("Querying purchases elapsed time: " + (System.currentTimeMillis() - time)
+ "ms");
// If there are subscriptions supported, we add subscription rows as well
if (areSubscriptionsSupported()) {
PurchasesResult subscriptionResult = mBillingClient.queryPurchases(SkuType.SUBS);
LOG.info("Querying purchases and subscriptions elapsed time: "
+ (System.currentTimeMillis() - time) + "ms");
LOG.info("Querying subscriptions result code: "
+ subscriptionResult.getResponseCode()
+ " res: " + subscriptionResult.getPurchasesList().size());
if (subscriptionResult.getResponseCode() == BillingResponseCode.OK) {
purchasesResult.getPurchasesList().addAll(
subscriptionResult.getPurchasesList());
} else {
LOG.error("Got an error response trying to query subscription purchases");
}
} else if (purchasesResult.getResponseCode() == BillingResponseCode.OK) {
LOG.info("Skipped subscription purchases query since they are not supported");
} else {
LOG.warn("queryPurchases() got an error response code: "
+ purchasesResult.getResponseCode());
}
onQueryPurchasesFinished(purchasesResult);
}
};
executeServiceRequest(queryToExecute);
}
public void startServiceConnection(final Runnable executeOnSuccess) {
mBillingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
int billingResponseCode = billingResult.getResponseCode();
LOG.debug("Setup finished. Response code: " + billingResponseCode);
mIsServiceConnected = billingResponseCode == BillingResponseCode.OK;
mBillingClientResponseCode = billingResponseCode;
mBillingClientResponseMessage = billingResult.getDebugMessage();
mBillingUpdatesListener.onBillingClientSetupFinished();
if (mIsServiceConnected) {
if (executeOnSuccess != null) {
executeOnSuccess.run();
}
}
}
@Override
public void onBillingServiceDisconnected() {
mIsServiceConnected = false;
mBillingUpdatesListener.onBillingClientSetupFinished();
}
});
}
private void executeServiceRequest(Runnable runnable) {
if (mIsServiceConnected) {
runnable.run();
} else {
// If billing service was disconnected, we try to reconnect 1 time.
startServiceConnection(runnable);
}
}
private boolean verifyValidSignature(String signedData, String signature) {
return Security.verifyPurchase(mSignatureBase64, signedData, signature);
}
}

View file

@ -1,43 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp.util;
/**
* Exception thrown when something went wrong with in-app billing.
* An IabException has an associated IabResult (an error).
* To get the IAB result that caused this exception to be thrown,
* call {@link #getResult()}.
*/
public class IabException extends Exception {
IabResult mResult;
public IabException(IabResult r) {
this(r, null);
}
public IabException(int response, String message) {
this(new IabResult(response, message));
}
public IabException(IabResult r, Exception cause) {
super(r.getMessage(), cause);
mResult = r;
}
public IabException(int response, String message, Exception cause) {
this(new IabResult(response, message), cause);
}
/** Returns the IAB result (error) that this exception signals. */
public IabResult getResult() { return mResult; }
}

File diff suppressed because it is too large Load diff

View file

@ -1,45 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp.util;
/**
* Represents the result of an in-app billing operation.
* A result is composed of a response code (an integer) and possibly a
* message (String). You can get those by calling
* {@link #getResponse} and {@link #getMessage()}, respectively. You
* can also inquire whether a result is a success or a failure by
* calling {@link #isSuccess()} and {@link #isFailure()}.
*/
public class IabResult {
int mResponse;
String mMessage;
public IabResult(int response, String message) {
mResponse = response;
if (message == null || message.trim().length() == 0) {
mMessage = IabHelper.getResponseDesc(response);
}
else {
mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
}
}
public int getResponse() { return mResponse; }
public String getMessage() { return mMessage; }
public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
public boolean isFailure() { return !isSuccess(); }
public String toString() { return "IabResult: " + getMessage(); }
}

View file

@ -1,91 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a block of information about in-app items.
* An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
*/
public class Inventory {
Map<String,SkuDetails> mSkuMap = new HashMap<String,SkuDetails>();
Map<String,Purchase> mPurchaseMap = new HashMap<String,Purchase>();
Inventory() { }
/** Returns the listing details for an in-app product. */
public SkuDetails getSkuDetails(String sku) {
return mSkuMap.get(sku);
}
/** Returns purchase information for a given product, or null if there is no purchase. */
public Purchase getPurchase(String sku) {
return mPurchaseMap.get(sku);
}
/** Returns whether or not there exists a purchase of the given product. */
public boolean hasPurchase(String sku) {
return mPurchaseMap.containsKey(sku);
}
/** Return whether or not details about the given product are available. */
public boolean hasDetails(String sku) {
return mSkuMap.containsKey(sku);
}
/**
* Erase a purchase (locally) from the inventory, given its product ID. This just
* modifies the Inventory object locally and has no effect on the server! This is
* useful when you have an existing Inventory object which you know to be up to date,
* and you have just consumed an item successfully, which means that erasing its
* purchase data from the Inventory you already have is quicker than querying for
* a new Inventory.
*/
public void erasePurchase(String sku) {
if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
}
/** Returns a list of all owned product IDs. */
public List<String> getAllOwnedSkus() {
return new ArrayList<String>(mPurchaseMap.keySet());
}
/** Returns a list of all owned product IDs of a given type */
public List<String> getAllOwnedSkus(String itemType) {
List<String> result = new ArrayList<String>();
for (Purchase p : mPurchaseMap.values()) {
if (p.getItemType().equals(itemType)) result.add(p.getSku());
}
return result;
}
/** Returns a list of all purchases. */
List<Purchase> getAllPurchases() {
return new ArrayList<Purchase>(mPurchaseMap.values());
}
void addSkuDetails(SkuDetails d) {
mSkuMap.put(d.getSku(), d);
}
void addPurchase(Purchase p) {
mPurchaseMap.put(p.getSku(), p);
}
}

View file

@ -1,63 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp.util;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Represents an in-app billing purchase.
*/
public class Purchase {
String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
String mOrderId;
String mPackageName;
String mSku;
long mPurchaseTime;
int mPurchaseState;
String mDeveloperPayload;
String mToken;
String mOriginalJson;
String mSignature;
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
mItemType = itemType;
mOriginalJson = jsonPurchaseInfo;
JSONObject o = new JSONObject(mOriginalJson);
mOrderId = o.optString("orderId");
mPackageName = o.optString("packageName");
mSku = o.optString("productId");
mPurchaseTime = o.optLong("purchaseTime");
mPurchaseState = o.optInt("purchaseState");
mDeveloperPayload = o.optString("developerPayload");
mToken = o.optString("token", o.optString("purchaseToken"));
mSignature = signature;
}
public String getItemType() { return mItemType; }
public String getOrderId() { return mOrderId; }
public String getPackageName() { return mPackageName; }
public String getSku() { return mSku; }
public long getPurchaseTime() { return mPurchaseTime; }
public int getPurchaseState() { return mPurchaseState; }
public String getDeveloperPayload() { return mDeveloperPayload; }
public String getToken() { return mToken; }
public String getOriginalJson() { return mOriginalJson; }
public String getSignature() { return mSignature; }
@Override
public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
}

View file

@ -18,10 +18,6 @@ package net.osmand.plus.inapp.util;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;

View file

@ -1,73 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.osmand.plus.inapp.util;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Represents an in-app product's listing details.
*/
public class SkuDetails {
String mItemType;
String mSku;
String mType;
String mPrice;
long mPriceAmountMicros;
String mPriceCurrencyCode;
String mSubscriptionPeriod;
String mTitle;
String mDescription;
String mJson;
public SkuDetails(String jsonSkuDetails) throws JSONException {
this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
}
public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
mItemType = itemType;
mJson = jsonSkuDetails;
JSONObject o = new JSONObject(mJson);
mSku = o.optString("productId");
mType = o.optString("type");
mPrice = o.optString("price");
mPriceAmountMicros = o.optLong("price_amount_micros");
mPriceCurrencyCode = o.optString("price_currency_code");
mSubscriptionPeriod = o.optString("subscriptionPeriod");
mTitle = o.optString("title");
mDescription = o.optString("description");
}
public String getSku() { return mSku; }
public String getType() { return mType; }
public String getPrice() { return mPrice; }
public long getPriceAmountMicros() {
return mPriceAmountMicros;
}
public String getPriceCurrencyCode() {
return mPriceCurrencyCode;
}
public String getSubscriptionPeriod() {
return mSubscriptionPeriod;
}
public String getTitle() { return mTitle; }
public String getDescription() { return mDescription; }
@Override
public String toString() {
return "SkuDetails:" + mJson;
}
}

View file

@ -88,6 +88,7 @@ public class MapContextMenu extends MenuTitleController implements StateChangedL
private LatLon mapCenter;
private int mapPosition = 0;
private boolean centerMarker;
private boolean zoomOutOnly;
private int mapZoom;
private boolean inLocationUpdate = false;
@ -286,6 +287,14 @@ public class MapContextMenu extends MenuTitleController implements StateChangedL
this.centerMarker = centerMarker;
}
public boolean isZoomOutOnly() {
return zoomOutOnly;
}
public void setZoomOutOnly(boolean zoomOutOnly) {
this.zoomOutOnly = zoomOutOnly;
}
public int getMapZoom() {
return mapZoom;
}

View file

@ -1698,7 +1698,7 @@ public class MapContextMenuFragment extends BaseOsmAndFragment implements Downlo
public void centerMarkerLocation() {
centered = true;
showOnMap(menu.getLatLon(), true, true, false, getZoom());
showOnMap(menu.getLatLon(), true, false, getZoom());
}
private int getZoom() {
@ -1722,8 +1722,8 @@ public class MapContextMenuFragment extends BaseOsmAndFragment implements Downlo
cp.setCenterLocation(0.5f, map.getMapPosition() == OsmandSettings.BOTTOM_CONSTANT ? 0.15f : 0.5f);
cp.setLatLonCenter(flat, flon);
cp.setZoom(zoom);
flat = cp.getLatFromPixel(cp.getPixWidth() / 2, cp.getPixHeight() / 2);
flon = cp.getLonFromPixel(cp.getPixWidth() / 2, cp.getPixHeight() / 2);
flat = cp.getLatFromPixel(cp.getPixWidth() / 2f, cp.getPixHeight() / 2f);
flon = cp.getLonFromPixel(cp.getPixWidth() / 2f, cp.getPixHeight() / 2f);
if (updateOrigXY) {
origMarkerX = cp.getCenterPixelX();
@ -1732,21 +1732,22 @@ public class MapContextMenuFragment extends BaseOsmAndFragment implements Downlo
return new LatLon(flat, flon);
}
private void showOnMap(LatLon latLon, boolean updateCoords, boolean needMove, boolean alreadyAdjusted, int zoom) {
private void showOnMap(LatLon latLon, boolean updateCoords, boolean alreadyAdjusted, int zoom) {
AnimateDraggingMapThread thread = map.getAnimatedDraggingThread();
int calculatedZoom = menu.isZoomOutOnly() ? thread.calculateMoveZoom(null, latLon.getLatitude(), latLon.getLongitude(), null) : 0;
if (calculatedZoom > 0) {
zoom = Math.min(zoom, calculatedZoom);
}
menu.setZoomOutOnly(false);
LatLon calcLatLon = calculateCenterLatLon(latLon, zoom, updateCoords);
if (updateCoords) {
mapCenter = calcLatLon;
menu.setMapCenter(mapCenter);
}
if (!alreadyAdjusted) {
calcLatLon = getAdjustedMarkerLocation(getPosY(), calcLatLon, true, zoom);
}
if (needMove) {
thread.startMoving(calcLatLon.getLatitude(), calcLatLon.getLongitude(), zoom, true);
}
thread.startMoving(calcLatLon.getLatitude(), calcLatLon.getLongitude(), zoom, true);
}
private void setAddressLocation() {
@ -2021,7 +2022,7 @@ public class MapContextMenuFragment extends BaseOsmAndFragment implements Downlo
}
if (animated) {
showOnMap(latlon, false, true, true, zoom);
showOnMap(latlon, false, true, zoom);
} else {
if (dZoom != 0) {
map.setIntZoom(zoom);

View file

@ -60,7 +60,7 @@ public class TransportRouteController extends MenuController {
public void buttonPressed() {
final int previousStop = getPreviousStop();
if (previousStop != -1) {
showTransportStop(getTransportRoute().route.getForwardStops().get(previousStop));
showTransportStop(getTransportRoute().route.getForwardStops().get(previousStop), true);
}
}
};
@ -72,7 +72,7 @@ public class TransportRouteController extends MenuController {
public void buttonPressed() {
final int nextStop = getNextStop();
if (nextStop != -1) {
showTransportStop(getTransportRoute().route.getForwardStops().get(nextStop));
showTransportStop(getTransportRoute().route.getForwardStops().get(nextStop), true);
}
}
};
@ -180,7 +180,7 @@ public class TransportRouteController extends MenuController {
}
}
private void showTransportStop(TransportStop stop) {
private void showTransportStop(TransportStop stop, boolean movingBetweenStops) {
MapActivity mapActivity = getMapActivity();
if (mapActivity != null && mapContextMenu != null) {
transportRoute.stop = stop;
@ -198,6 +198,7 @@ public class TransportRouteController extends MenuController {
mapContextMenu.setMapPosition(getMapActivity().getMapView().getMapPosition());
}
mapContextMenu.setCenterMarker(true);
mapContextMenu.setZoomOutOnly(movingBetweenStops);
mapContextMenu.setMapZoom(15);
mapContextMenu.showOrUpdate(stopLocation, pd, transportRoute);
}
@ -286,16 +287,7 @@ public class TransportRouteController extends MenuController {
@Override
public void onClick(View arg0) {
showTransportStop(stop);
/*
PointDescription pd = new PointDescription(PointDescription.POINT_TYPE_TRANSPORT_STOP,
getMapActivity().getString(R.string.transport_Stop), name);
LatLon stopLocation = stop.getLocation();
getMapActivity().getMyApplication().getSettings()
.setMapLocationToShow(stopLocation.getLatitude(), stopLocation.getLongitude(),
15, pd, false, transportRoute);
MapActivity.launchMapActivityMoveToTop(getMapActivity());
*/
showTransportStop(stop, false);
}
});
}

View file

@ -161,7 +161,7 @@ public class TransportStopController extends MenuController {
private void addTransportStopRoutes(OsmandApplication app, List<TransportStop> stops, List<TransportStopRoute> routes, boolean useEnglishNames) {
for (TransportStop tstop : stops) {
if (tstop.hasReferencesToRoutes()) {
if (tstop.hasReferencesToRoutesMap()) {
addRoutes(app, routes, useEnglishNames, tstop, transportStop, (int) MapUtils.getDistance(tstop.getLocation(), transportStop.getLocation()));
}
}
@ -262,15 +262,19 @@ public class TransportStopController extends MenuController {
private static void processTransportStopAggregated(OsmandApplication app, TransportStop transportStop) {
TransportStopAggregated stopAggregated = new TransportStopAggregated();
transportStop.setTransportStopAggregated(stopAggregated);
stopAggregated.addLocalTransportStop(transportStop);
TransportStop localStop = null;
LatLon loc = transportStop.getLocation();
List<TransportStop> transportStops = findTransportStopsAt(app, loc.getLatitude(), loc.getLongitude(), SHOW_STOPS_RADIUS_METERS);
if (transportStops != null) {
for (TransportStop stop : transportStops) {
stopAggregated.addNearbyTransportStop(stop);
if (localStop == null && transportStop.equals(stop)) {
localStop = stop;
} else {
stopAggregated.addNearbyTransportStop(stop);
}
}
}
stopAggregated.addLocalTransportStop(localStop == null ? transportStop : localStop);
}
private static TransportStopAggregated processTransportStopsForAmenity(List<TransportStop> transportStops, Amenity amenity) {

View file

@ -26,7 +26,6 @@ public abstract class OsmandNotification {
public final static int WEAR_ERROR_NOTIFICATION_SERVICE_ID = 1007;
public final static int WEAR_DOWNLOAD_NOTIFICATION_SERVICE_ID = 1008;
protected OsmandApplication app;
protected boolean ongoing = true;
protected int color;
@ -35,6 +34,8 @@ public abstract class OsmandNotification {
private String groupName;
private Notification currentNotification;
public enum NotificationType {
NAVIGATION,
GPX,
@ -125,7 +126,7 @@ public abstract class OsmandNotification {
if (isEnabled()) {
Builder notificationBuilder = buildNotification(false);
if (notificationBuilder != null) {
Notification notification = notificationBuilder.build();
Notification notification = getNotification(notificationBuilder);
setupNotification(notification);
notificationManager.notify(top ? TOP_NOTIFICATION_SERVICE_ID : getOsmandNotificationId(), notification);
notifyWearable(notificationManager);
@ -140,10 +141,10 @@ public abstract class OsmandNotification {
if (isEnabled()) {
Builder notificationBuilder = buildNotification(false);
if (notificationBuilder != null) {
Notification notification = notificationBuilder.build();
Notification notification = getNotification(notificationBuilder);
setupNotification(notification);
if (top) {
notificationManager.cancel(getOsmandNotificationId());
//notificationManager.cancel(getOsmandNotificationId());
notificationManager.notify(TOP_NOTIFICATION_SERVICE_ID, notification);
} else {
notificationManager.notify(getOsmandNotificationId(), notification);
@ -159,7 +160,17 @@ public abstract class OsmandNotification {
return false;
}
private Notification getNotification(Builder notificationBuilder) {
Notification notification = currentNotification;
if (notification == null) {
notification = notificationBuilder.build();
currentNotification = notification;
}
return notification;
}
public void removeNotification() {
currentNotification = null;
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app);
notificationManager.cancel(getOsmandNotificationId());
notificationManager.cancel(getOsmandWearableNotificationId());

View file

@ -99,6 +99,7 @@ public class ChooseRouteFragment extends BaseOsmAndFragment implements ContextMe
private boolean wasDrawerDisabled;
private int currentMenuState;
private int routesCount;
private boolean paused;
private boolean publicTransportMode;
private boolean needAdjustMap;
@ -209,6 +210,7 @@ public class ChooseRouteFragment extends BaseOsmAndFragment implements ContextMe
@Override
public void onResume() {
super.onResume();
paused = false;
MapActivity mapActivity = getMapActivity();
if (mapActivity != null) {
mapActivity.getMapLayers().getMapControlsLayer().showMapControlsIfHidden();
@ -223,6 +225,7 @@ public class ChooseRouteFragment extends BaseOsmAndFragment implements ContextMe
public void onPause() {
super.onPause();
paused = true;
MapRouteInfoMenu.chooseRoutesVisible = false;
MapActivity mapActivity = getMapActivity();
if (mapActivity != null) {
@ -262,6 +265,10 @@ public class ChooseRouteFragment extends BaseOsmAndFragment implements ContextMe
return getIcon(id, nightMode ? R.color.icon_color_default_dark : R.color.icon_color_default_light);
}
public boolean isPaused() {
return paused;
}
public void analyseOnMap(LatLon location, GpxDisplayItem gpxItem) {
OsmandApplication app = requireMyApplication();
final OsmandSettings settings = app.getSettings();

View file

@ -543,6 +543,7 @@ public class MapRouteInfoMenu implements IRouteInformationListener, CardListener
card.setRouteButtonsVisible(false);
card.setShowBottomShadow(false);
card.setShowTopShadow(false);
card.setListener(this);
menuCards.add(card);
hasTopCard = true;
}
@ -668,6 +669,10 @@ public class MapRouteInfoMenu implements IRouteInformationListener, CardListener
if (card instanceof SimpleRouteCard) {
hide();
ChooseRouteFragment.showInstance(mapActivity.getSupportFragmentManager(), 0, MenuState.FULL_SCREEN);
} else if (card instanceof PublicTransportCard) {
hide();
ChooseRouteFragment.showInstance(mapActivity.getSupportFragmentManager(),
((PublicTransportCard) card).getRouteId(), MenuState.FULL_SCREEN);
}
}
}
@ -1885,6 +1890,7 @@ public class MapRouteInfoMenu implements IRouteInformationListener, CardListener
return false;
}
@Nullable
public WeakReference<MapRouteInfoMenuFragment> findMenuFragment() {
MapActivity mapActivity = getMapActivity();
if (mapActivity != null) {
@ -1896,6 +1902,18 @@ public class MapRouteInfoMenu implements IRouteInformationListener, CardListener
return null;
}
@Nullable
public WeakReference<ChooseRouteFragment> findChooseRouteFragment() {
MapActivity mapActivity = getMapActivity();
if (mapActivity != null) {
Fragment fragment = mapActivity.getSupportFragmentManager().findFragmentByTag(ChooseRouteFragment.TAG);
if (fragment instanceof ChooseRouteFragment && !((ChooseRouteFragment) fragment).isPaused()) {
return new WeakReference<>((ChooseRouteFragment) fragment);
}
}
return null;
}
public static void showLocationOnMap(MapActivity mapActivity, double latitude, double longitude) {
RotatedTileBox tb = mapActivity.getMapView().getCurrentRotatedTileBox().copy();
int tileBoxWidthPx = 0;

View file

@ -106,11 +106,33 @@ public class PublicTransportCard extends BaseCard {
@Override
protected void updateContent() {
List<TransportRouteResultSegment> segments = routeResult.getSegments();
createRouteBadges(segments);
boolean badgesRowClickable = !routeInfoVisible && !routeButtonsVisible;
createRouteBadges(segments, badgesRowClickable);
view.findViewById(R.id.route_info).setVisibility(routeInfoVisible ? View.VISIBLE : View.GONE);
view.findViewById(R.id.route_buttons).setVisibility(routeButtonsVisible ? View.VISIBLE : View.GONE);
view.findViewById(R.id.badges_padding).setVisibility(!routeInfoVisible && !routeButtonsVisible ? View.VISIBLE : View.GONE);
if (badgesRowClickable) {
view.findViewById(R.id.badges_padding).setVisibility(View.VISIBLE);
view.setBackgroundResource(AndroidUtils.resolveAttribute(view.getContext(), R.attr.card_and_list_background_basic));
View info = view.findViewById(R.id.routes_info_container);
int paddingLeft = info.getPaddingLeft();
int paddingTop = info.getPaddingTop();
int paddingRight = info.getPaddingRight();
int paddingBottom = info.getPaddingBottom();
info.setBackgroundResource(AndroidUtils.resolveAttribute(view.getContext(), android.R.attr.selectableItemBackground));
info.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
info.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CardListener listener = getListener();
if (listener != null) {
listener.onCardPressed(PublicTransportCard.this);
}
}
});
} else {
view.findViewById(R.id.badges_padding).setVisibility(View.GONE);
}
TextView fromLine = (TextView) view.findViewById(R.id.from_line);
TextView wayLine = (TextView) view.findViewById(R.id.way_line);
@ -256,7 +278,7 @@ public class PublicTransportCard extends BaseCard {
return secondLineDesc;
}
private void createRouteBadges(List<TransportRouteResultSegment> segments) {
private void createRouteBadges(List<TransportRouteResultSegment> segments, boolean badgesRowClickable) {
int itemsSpacing = AndroidUtils.dpToPx(app, 6);
FlowLayout routesBadges = (FlowLayout) view.findViewById(R.id.routes_badges);
routesBadges.removeAllViews();
@ -270,7 +292,7 @@ public class PublicTransportCard extends BaseCard {
if (walkingSegment != null) {
double walkTime = walkingSegment.getRoutingTime();
if (walkTime > MIN_WALK_TIME) {
routesBadges.addView(createWalkRouteBadge(walkingSegment), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
routesBadges.addView(createWalkRouteBadge(walkingSegment, badgesRowClickable), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
routesBadges.addView(createArrow(), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
}
} else if (s.walkDist > 0) {
@ -283,11 +305,11 @@ public class PublicTransportCard extends BaseCard {
} else {
start = this.startLocation;
}
routesBadges.addView(createWalkRouteBadge(walkTime, start, end), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
routesBadges.addView(createWalkRouteBadge(walkTime, start, end, badgesRowClickable), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
routesBadges.addView(createArrow(), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
}
}
routesBadges.addView(createRouteBadge(s), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
routesBadges.addView(createRouteBadge(s, badgesRowClickable), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
if (iterator.hasNext()) {
routesBadges.addView(createArrow(), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
} else {
@ -296,7 +318,7 @@ public class PublicTransportCard extends BaseCard {
double walkTime = walkingSegment.getRoutingTime();
if (walkTime > MIN_WALK_TIME) {
routesBadges.addView(createArrow(), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
routesBadges.addView(createWalkRouteBadge(walkingSegment), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
routesBadges.addView(createWalkRouteBadge(walkingSegment, badgesRowClickable), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
}
} else {
double finishWalkDist = routeResult.getFinishWalkDist();
@ -306,7 +328,7 @@ public class PublicTransportCard extends BaseCard {
LatLon start = s.getEnd().getLocation();
LatLon end = this.endLocation;
routesBadges.addView(createArrow(), new FlowLayout.LayoutParams(itemsSpacing, itemsSpacing));
routesBadges.addView(createWalkRouteBadge(walkTime, start, end));
routesBadges.addView(createWalkRouteBadge(walkTime, start, end, badgesRowClickable));
}
}
}
@ -315,7 +337,7 @@ public class PublicTransportCard extends BaseCard {
}
}
private View createRouteBadge(@NonNull final TransportRouteResultSegment segment) {
private View createRouteBadge(@NonNull final TransportRouteResultSegment segment, boolean badgesRowClickable) {
LinearLayout bageView = (LinearLayout) mapActivity.getLayoutInflater().inflate(R.layout.transport_stop_route_item_with_icon, null, false);
TransportRoute transportRoute = segment.route;
TransportStopRoute transportStopRoute = TransportStopRoute.getTransportStopRoute(transportRoute, segment.getStart());
@ -333,20 +355,22 @@ public class PublicTransportCard extends BaseCard {
gradientDrawableBg.setColor(bgColor);
transportStopRouteTextView.setTextColor(UiUtilities.getContrastColor(app, bgColor, true));
if (transportCardListener != null) {
if (transportCardListener != null && !badgesRowClickable) {
bageView.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
transportCardListener.onPublicTransportCardBadgePressed(PublicTransportCard.this, segment);
}
});
} else {
bageView.findViewById(R.id.button).setBackgroundDrawable(null);
}
return bageView;
}
private View createWalkRouteBadge(@NonNull final RouteCalculationResult result) {
View v = createWalkRouteBadge(result.getRoutingTime(), null, null);
if (transportCardListener != null) {
private View createWalkRouteBadge(@NonNull final RouteCalculationResult result, boolean badgesRowClickable) {
View v = createWalkRouteBadge(result.getRoutingTime(), null, null, badgesRowClickable);
if (transportCardListener != null && !badgesRowClickable) {
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -357,7 +381,7 @@ public class PublicTransportCard extends BaseCard {
return v;
}
private View createWalkRouteBadge(double walkTime, @Nullable final LatLon start, @Nullable final LatLon end) {
private View createWalkRouteBadge(double walkTime, @Nullable final LatLon start, @Nullable final LatLon end, boolean badgesRowClickable) {
LinearLayout bageView = (LinearLayout) getMapActivity().getLayoutInflater().inflate(R.layout.transport_stop_route_item_with_icon, null, false);
int activeColor = getActiveColor();
@ -372,13 +396,15 @@ public class PublicTransportCard extends BaseCard {
AndroidUtils.setBackground(app, bageView, nightMode, R.drawable.btn_border_active_light, R.drawable.btn_border_active_dark);
if (transportCardListener != null && start != null && end != null) {
if (transportCardListener != null && start != null && end != null && !badgesRowClickable) {
bageView.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
transportCardListener.onPublicTransportCardBadgePressed(PublicTransportCard.this, start, end);
}
});
} else {
bageView.findViewById(R.id.button).setBackgroundDrawable(null);
}
return bageView;
}

View file

@ -180,6 +180,7 @@ public class AnimateDraggingMapThread {
public void startMoving(final double finalLat, final double finalLon, final int endZoom,
final boolean notifyListener, final Runnable finishAnimationCallback) {
boolean wasAnimating = isAnimating();
stopAnimatingSync();
final RotatedTileBox rb = tileView.getCurrentRotatedTileBox().copy();
@ -187,22 +188,12 @@ public class AnimateDraggingMapThread {
double startLon = rb.getLongitude();
final int startZoom = rb.getZoom();
final double startZoomFP = rb.getZoomFloatPart();
boolean skipAnimation = false;
float mStX = rb.getPixXFromLatLon(startLat, startLon) - rb.getPixXFromLatLon(finalLat, finalLon);
float mStY = rb.getPixYFromLatLon(startLat, startLon) - rb.getPixYFromLatLon(finalLat, finalLon);
while (Math.abs(mStX) + Math.abs(mStY) > 1200) {
rb.setZoom(rb.getZoom() - 1);
if (rb.getZoom() <= 4) {
skipAnimation = true;
}
mStX = rb.getPixXFromLatLon(startLat, startLon) - rb.getPixXFromLatLon(finalLat, finalLon);
mStY = rb.getPixYFromLatLon(startLat, startLon) - rb.getPixYFromLatLon(finalLat, finalLon);
}
final int moveZoom = rb.getZoom();
float[] mSt = new float[2];
final int moveZoom = calculateMoveZoom(rb, finalLat, finalLon, mSt);
boolean skipAnimation = moveZoom == 0;
// check if animation needed
skipAnimation = skipAnimation || (Math.abs(moveZoom - startZoom) >= 3 || Math.abs(endZoom - moveZoom) > 3);
if (skipAnimation) {
if (skipAnimation || wasAnimating) {
tileView.setLatLonAnimate(finalLat, finalLon, notifyListener);
tileView.setFractionalZoom(endZoom, 0, notifyListener);
if (finishAnimationCallback != null) {
@ -219,7 +210,7 @@ public class AnimateDraggingMapThread {
final float mMoveY = rb.getPixYFromLatLon(startLat, startLon) - rb.getPixYFromLatLon(finalLat, finalLon);
final boolean doNotUseAnimations = tileView.getSettings().DO_NOT_USE_ANIMATIONS.get();
final float animationTime = doNotUseAnimations ? 1 : Math.max(450, (Math.abs(mStX) + Math.abs(mStY)) / 1200f * MOVE_MOVE_ANIMATION_TIME);
final float animationTime = doNotUseAnimations ? 1 : Math.max(450, (Math.abs(mSt[0]) + Math.abs(mSt[1])) / 1200f * MOVE_MOVE_ANIMATION_TIME);
startThreadAnimating(new Runnable() {
@ -248,6 +239,30 @@ public class AnimateDraggingMapThread {
});
}
public int calculateMoveZoom(RotatedTileBox rb, final double finalLat, final double finalLon, float[] mSt) {
if (rb == null) {
rb = tileView.getCurrentRotatedTileBox().copy();
}
double startLat = rb.getLatitude();
double startLon = rb.getLongitude();
boolean skipAnimation = false;
if (mSt == null) {
mSt = new float[2];
}
mSt[0] = rb.getPixXFromLatLon(startLat, startLon) - rb.getPixXFromLatLon(finalLat, finalLon);
mSt[1] = rb.getPixYFromLatLon(startLat, startLon) - rb.getPixYFromLatLon(finalLat, finalLon);
while (Math.abs(mSt[0]) + Math.abs(mSt[1]) > 1200) {
rb.setZoom(rb.getZoom() - 1);
if (rb.getZoom() <= 4) {
skipAnimation = true;
}
mSt[0] = rb.getPixXFromLatLon(startLat, startLon) - rb.getPixXFromLatLon(finalLat, finalLon);
mSt[1] = rb.getPixYFromLatLon(startLat, startLon) - rb.getPixYFromLatLon(finalLat, finalLon);
}
return skipAnimation ? 0 : rb.getZoom();
}
private void animatingRotateInThread(float rotate, float animationTime, boolean notify) {
AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
float startRotate = tileView.getRotate();

View file

@ -59,12 +59,14 @@ import net.osmand.plus.mapcontextmenu.controllers.TransportStopController;
import net.osmand.plus.mapcontextmenu.other.MapMultiSelectionMenu;
import net.osmand.plus.render.MapRenderRepositories;
import net.osmand.plus.render.NativeOsmandLibrary;
import net.osmand.plus.routepreparationmenu.ChooseRouteFragment;
import net.osmand.plus.routepreparationmenu.MapRouteInfoMenu;
import net.osmand.plus.views.AddGpxPointBottomSheetHelper.NewGpxPoint;
import net.osmand.plus.views.corenative.NativeCoreContext;
import net.osmand.util.Algorithms;
import net.osmand.util.MapUtils;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -962,6 +964,16 @@ public class ContextMenuLayer extends OsmandMapLayer {
boolean processed = hideVisibleMenues();
processed |= menu.onSingleTapOnMap();
if (!processed && MapRouteInfoMenu.chooseRoutesVisible) {
WeakReference<ChooseRouteFragment> chooseRouteFragmentRef = activity.getMapRouteInfoMenu().findChooseRouteFragment();
if (chooseRouteFragmentRef != null) {
ChooseRouteFragment chooseRouteFragment = chooseRouteFragmentRef.get();
if (chooseRouteFragment != null) {
chooseRouteFragment.dismiss();
processed = true;
}
}
}
if (!processed && activity.getMyApplication().getSettings().MAP_EMPTY_STATE_ALLOWED.get()) {
activity.getMapLayers().getMapControlsLayer().switchMapControlsVisibility(true);
}