Compare commits
35 commits
Author | SHA1 | Date | |
---|---|---|---|
|
fa5425c8fe | ||
|
8352fa4d84 | ||
|
ad4fa9c500 | ||
|
fae21cbdf3 | ||
|
784ca0df66 | ||
|
d2ea48e468 | ||
|
98162d6b8f | ||
|
1505b77fea | ||
|
184c7d3b90 | ||
|
556d75c6f2 | ||
|
5bc0da0efb | ||
|
cd4bb2667c | ||
|
dd296864f4 | ||
|
1f04ce2ce2 | ||
|
3a7232c8b4 | ||
|
53acf00f1a | ||
|
8235f31f7d | ||
|
4353251067 | ||
|
30056e5dea | ||
|
ef52f119d7 | ||
|
222b9e410b | ||
|
0808d1a3b8 | ||
|
5eac1ac31d | ||
|
4e5d5f700f | ||
|
f0eae1515a | ||
|
1ae1893105 | ||
|
ceff1bef41 | ||
|
e04a63987c | ||
|
e11ee96e6a | ||
|
1d316087b6 | ||
|
f0dde03f19 | ||
|
ba6d3c1c85 | ||
|
5e591194c0 | ||
|
46b88489a3 | ||
|
e86bde9c61 |
48 changed files with 1743 additions and 2558 deletions
|
@ -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'
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
171
OsmAnd-java/src/main/java/net/osmand/Period.java
Normal file
171
OsmAnd-java/src/main/java/net/osmand/Period.java
Normal file
|
@ -0,0 +1,171 @@
|
|||
package net.osmand;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Period {
|
||||
|
||||
public enum PeriodUnit {
|
||||
YEAR("Y"),
|
||||
MONTH("M"),
|
||||
WEEK("W"),
|
||||
DAY("D");
|
||||
|
||||
private String unitStr;
|
||||
|
||||
PeriodUnit(String unitStr) {
|
||||
this.unitStr = unitStr;
|
||||
}
|
||||
|
||||
public String getUnitStr() {
|
||||
return unitStr;
|
||||
}
|
||||
|
||||
public double getMonthsValue() {
|
||||
switch (this) {
|
||||
case YEAR:
|
||||
return 12d;
|
||||
case MONTH:
|
||||
return 1d;
|
||||
case WEEK:
|
||||
return 1/4d;
|
||||
case DAY:
|
||||
return 1/30d;
|
||||
}
|
||||
return 0d;
|
||||
}
|
||||
|
||||
public static PeriodUnit parseUnit(String unitStr) {
|
||||
for (PeriodUnit unit : values()) {
|
||||
if (unit.unitStr.equals(unitStr)) {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Pattern PATTERN =
|
||||
Pattern.compile("^P(?:([-+]?[0-9]+)([YMWD]))?$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private PeriodUnit unit;
|
||||
|
||||
private final int numberOfUnits;
|
||||
|
||||
public static Period ofYears(int years) {
|
||||
return new Period(PeriodUnit.YEAR, years);
|
||||
}
|
||||
|
||||
public static Period ofMonths(int months) {
|
||||
return new Period(PeriodUnit.MONTH, months);
|
||||
}
|
||||
|
||||
public static Period ofWeeks(int weeks) {
|
||||
return new Period(PeriodUnit.WEEK, weeks);
|
||||
}
|
||||
|
||||
public static Period ofDays(int days) {
|
||||
return new Period(PeriodUnit.DAY, days);
|
||||
}
|
||||
|
||||
public PeriodUnit getUnit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
public int getNumberOfUnits() {
|
||||
return numberOfUnits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a {@code Period} from a text string such as {@code PnY PnM PnD PnW}.
|
||||
* <p>
|
||||
* This will parse the string produced by {@code toString()} which is
|
||||
* based on the ISO-8601 period formats {@code PnY PnM PnD PnW}.
|
||||
* <p>
|
||||
* The string cannot start with negative sign.
|
||||
* The ASCII letter "P" is next in upper or lower case.
|
||||
* <p>
|
||||
* For example, the following are valid inputs:
|
||||
* <pre>
|
||||
* "P2Y" -- Period.ofYears(2)
|
||||
* "P3M" -- Period.ofMonths(3)
|
||||
* "P4W" -- Period.ofWeeks(4)
|
||||
* "P5D" -- Period.ofDays(5)
|
||||
* </pre>
|
||||
*
|
||||
* @param text the text to parse, not null
|
||||
* @return the parsed period, not null
|
||||
* @throws ParseException if the text cannot be parsed to a period
|
||||
*/
|
||||
public static Period parse(CharSequence text) throws ParseException {
|
||||
Matcher matcher = PATTERN.matcher(text);
|
||||
if (matcher.matches()) {
|
||||
String numberOfUnitsMatch = matcher.group(1);
|
||||
String unitMatch = matcher.group(2);
|
||||
if (numberOfUnitsMatch != null && unitMatch != null) {
|
||||
try {
|
||||
int numberOfUnits = parseNumber(numberOfUnitsMatch);
|
||||
PeriodUnit unit = PeriodUnit.parseUnit(unitMatch);
|
||||
return new Period(unit, numberOfUnits);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new ParseException("Text cannot be parsed to a Period: " + text, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new ParseException("Text cannot be parsed to a Period: " + text, 0);
|
||||
}
|
||||
|
||||
private static int parseNumber(String str) throws ParseException {
|
||||
if (str == null) {
|
||||
return 0;
|
||||
}
|
||||
return Integer.parseInt(str);
|
||||
}
|
||||
|
||||
public Period(PeriodUnit unit, int numberOfUnits) {
|
||||
if (unit == null) {
|
||||
throw new IllegalArgumentException("PeriodUnit cannot be null");
|
||||
}
|
||||
this.unit = unit;
|
||||
this.numberOfUnits = numberOfUnits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj instanceof Period) {
|
||||
Period other = (Period) obj;
|
||||
return unit.ordinal() == other.unit.ordinal() && numberOfUnits == other.numberOfUnits;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return unit.ordinal() + Integer.rotateLeft(numberOfUnits, 8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append('P').append(numberOfUnits);
|
||||
switch (unit) {
|
||||
case YEAR:
|
||||
buf.append('Y');
|
||||
break;
|
||||
case MONTH:
|
||||
buf.append('M');
|
||||
break;
|
||||
case WEEK:
|
||||
buf.append('W');
|
||||
break;
|
||||
case DAY:
|
||||
buf.append('D');
|
||||
break;
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
|
@ -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
5
OsmAnd/.gitignore
vendored
|
@ -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/
|
||||
|
|
25
OsmAnd/AndroidManifest-huawei.xml
Normal file
25
OsmAnd/AndroidManifest-huawei.xml
Normal 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>
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
66
OsmAnd/src/net/osmand/plus/HuaweiDrmHelper.java
Normal file
66
OsmAnd/src/net/osmand/plus/HuaweiDrmHelper.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
420
OsmAnd/src/net/osmand/plus/inapp/util/BillingManager.java
Normal file
420
OsmAnd/src/net/osmand/plus/inapp/util/BillingManager.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
@ -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(); }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue