mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Migrate location and language module from Java to Kotlin (#5988)
* Rename .java to .kt * Migrated location and language module from Java to Kotlin * Changed lastLocation visibility
This commit is contained in:
parent
771f370f9a
commit
8265cc6306
15 changed files with 773 additions and 827 deletions
|
|
@ -423,7 +423,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback {
|
|||
* Moves map to GPS location
|
||||
*/
|
||||
private fun moveMapToGPSLocation() {
|
||||
locationManager.lastLocation?.let {
|
||||
locationManager.getLastLocation()?.let {
|
||||
moveMapTo(GeoPoint(it.latitude, it.longitude))
|
||||
}
|
||||
}
|
||||
|
|
@ -591,7 +591,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback {
|
|||
|
||||
override fun onLocationPermissionGranted() {
|
||||
if (moveToCurrentLocation || activity != "MediaActivity") {
|
||||
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn) {
|
||||
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
||||
locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
|
||||
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER)
|
||||
addMarkerAtGPSLocation()
|
||||
|
|
@ -606,7 +606,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback {
|
|||
* Adds a marker at the user's GPS location
|
||||
*/
|
||||
private fun addMarkerAtGPSLocation() {
|
||||
locationManager.lastLocation?.let {
|
||||
locationManager.getLastLocation()?.let {
|
||||
addLocationMarker(GeoPoint(it.latitude, it.longitude))
|
||||
markerImage.translationY = 0f
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
package fr.free.nrw.commons.language;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Immutable look up table for all app supported languages. All article languages may not be
|
||||
* present in this table as it is statically bundled with the app. */
|
||||
public class AppLanguageLookUpTable {
|
||||
public static final String SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans";
|
||||
public static final String TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant";
|
||||
public static final String CHINESE_CN_LANGUAGE_CODE = "zh-cn";
|
||||
public static final String CHINESE_HK_LANGUAGE_CODE = "zh-hk";
|
||||
public static final String CHINESE_MO_LANGUAGE_CODE = "zh-mo";
|
||||
public static final String CHINESE_SG_LANGUAGE_CODE = "zh-sg";
|
||||
public static final String CHINESE_TW_LANGUAGE_CODE = "zh-tw";
|
||||
public static final String CHINESE_YUE_LANGUAGE_CODE = "zh-yue";
|
||||
public static final String CHINESE_LANGUAGE_CODE = "zh";
|
||||
public static final String NORWEGIAN_LEGACY_LANGUAGE_CODE = "no";
|
||||
public static final String NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb";
|
||||
public static final String TEST_LANGUAGE_CODE = "test";
|
||||
public static final String FALLBACK_LANGUAGE_CODE = "en"; // Must exist in preference_language_keys.
|
||||
|
||||
@NonNull private final Resources resources;
|
||||
|
||||
// Language codes for all app supported languages in fixed order. The special code representing
|
||||
// the dynamic system language is null.
|
||||
@NonNull private SoftReference<List<String>> codesRef = new SoftReference<>(null);
|
||||
|
||||
// English names for all app supported languages in fixed order.
|
||||
@NonNull private SoftReference<List<String>> canonicalNamesRef = new SoftReference<>(null);
|
||||
|
||||
// Native names for all app supported languages in fixed order.
|
||||
@NonNull private SoftReference<List<String>> localizedNamesRef = new SoftReference<>(null);
|
||||
|
||||
public AppLanguageLookUpTable(@NonNull Context context) {
|
||||
resources = context.getResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Nonnull immutable list. The special code representing the dynamic system language is
|
||||
* null.
|
||||
*/
|
||||
@NonNull
|
||||
public List<String> getCodes() {
|
||||
List<String> codes = codesRef.get();
|
||||
if (codes == null) {
|
||||
codes = getStringList(R.array.preference_language_keys);
|
||||
codesRef = new SoftReference<>(codes);
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getCanonicalName(@Nullable String code) {
|
||||
String name = defaultIndex(getCanonicalNames(), indexOfCode(code), null);
|
||||
if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) {
|
||||
if (code.equals(Locale.CHINESE.getLanguage())) {
|
||||
name = Locale.CHINESE.getDisplayName(Locale.ENGLISH);
|
||||
} else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) {
|
||||
name = defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getLocalizedName(@Nullable String code) {
|
||||
String name = defaultIndex(getLocalizedNames(), indexOfCode(code), null);
|
||||
if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) {
|
||||
if (code.equals(Locale.CHINESE.getLanguage())) {
|
||||
name = Locale.CHINESE.getDisplayName(Locale.CHINESE);
|
||||
} else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) {
|
||||
name = defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<String> getCanonicalNames() {
|
||||
List<String> names = canonicalNamesRef.get();
|
||||
if (names == null) {
|
||||
names = getStringList(R.array.preference_language_canonical_names);
|
||||
canonicalNamesRef = new SoftReference<>(names);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public List<String> getLocalizedNames() {
|
||||
List<String> names = localizedNamesRef.get();
|
||||
if (names == null) {
|
||||
names = getStringList(R.array.preference_language_local_names);
|
||||
localizedNamesRef = new SoftReference<>(names);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public boolean isSupportedCode(@Nullable String code) {
|
||||
return getCodes().contains(code);
|
||||
}
|
||||
|
||||
private <T> T defaultIndex(List<T> list, int index, T defaultValue) {
|
||||
return inBounds(list, index) ? list.get(index) : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches #codes for the specified language code and returns the index for use in
|
||||
* #canonicalNames and #localizedNames.
|
||||
*
|
||||
* @param code The language code to search for. The special code representing the dynamic system
|
||||
* language is null.
|
||||
* @return The index of the language code or -1 if the code is not supported.
|
||||
*/
|
||||
private int indexOfCode(@Nullable String code) {
|
||||
return getCodes().indexOf(code);
|
||||
}
|
||||
|
||||
/** @return Nonnull immutable list. */
|
||||
@NonNull
|
||||
private List<String> getStringList(int id) {
|
||||
return Arrays.asList(getStringArray(id));
|
||||
}
|
||||
|
||||
private boolean inBounds(List<?> list, int index) {
|
||||
return index >= 0 && index < list.size();
|
||||
}
|
||||
|
||||
public String[] getStringArray(@ArrayRes int id) {
|
||||
return resources.getStringArray(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package fr.free.nrw.commons.language
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.text.TextUtils
|
||||
|
||||
import androidx.annotation.ArrayRes
|
||||
import fr.free.nrw.commons.R
|
||||
import java.lang.ref.SoftReference
|
||||
import java.util.Arrays
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
/** Immutable look up table for all app supported languages. All article languages may not be
|
||||
* present in this table as it is statically bundled with the app. */
|
||||
class AppLanguageLookUpTable(context: Context) {
|
||||
|
||||
companion object {
|
||||
const val SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans"
|
||||
const val TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant"
|
||||
const val CHINESE_CN_LANGUAGE_CODE = "zh-cn"
|
||||
const val CHINESE_HK_LANGUAGE_CODE = "zh-hk"
|
||||
const val CHINESE_MO_LANGUAGE_CODE = "zh-mo"
|
||||
const val CHINESE_SG_LANGUAGE_CODE = "zh-sg"
|
||||
const val CHINESE_TW_LANGUAGE_CODE = "zh-tw"
|
||||
const val CHINESE_YUE_LANGUAGE_CODE = "zh-yue"
|
||||
const val CHINESE_LANGUAGE_CODE = "zh"
|
||||
const val NORWEGIAN_LEGACY_LANGUAGE_CODE = "no"
|
||||
const val NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb"
|
||||
const val TEST_LANGUAGE_CODE = "test"
|
||||
const val FALLBACK_LANGUAGE_CODE = "en" // Must exist in preference_language_keys.
|
||||
}
|
||||
|
||||
private val resources: Resources = context.resources
|
||||
|
||||
// Language codes for all app supported languages in fixed order. The special code representing
|
||||
// the dynamic system language is null.
|
||||
private var codesRef = SoftReference<List<String>>(null)
|
||||
|
||||
// English names for all app supported languages in fixed order.
|
||||
private var canonicalNamesRef = SoftReference<List<String>>(null)
|
||||
|
||||
// Native names for all app supported languages in fixed order.
|
||||
private var localizedNamesRef = SoftReference<List<String>>(null)
|
||||
|
||||
/**
|
||||
* @return Nonnull immutable list. The special code representing the dynamic system language is
|
||||
* null.
|
||||
*/
|
||||
fun getCodes(): List<String> {
|
||||
var codes = codesRef.get()
|
||||
if (codes == null) {
|
||||
codes = getStringList(R.array.preference_language_keys)
|
||||
codesRef = SoftReference(codes)
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
fun getCanonicalName(code: String?): String? {
|
||||
var name = defaultIndex(getCanonicalNames(), indexOfCode(code), null)
|
||||
if (name.isNullOrEmpty() && !code.isNullOrEmpty()) {
|
||||
name = when (code) {
|
||||
Locale.CHINESE.language -> Locale.CHINESE.getDisplayName(Locale.ENGLISH)
|
||||
NORWEGIAN_LEGACY_LANGUAGE_CODE ->
|
||||
defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
fun getLocalizedName(code: String?): String? {
|
||||
var name = defaultIndex(getLocalizedNames(), indexOfCode(code), null)
|
||||
if (name.isNullOrEmpty() && !code.isNullOrEmpty()) {
|
||||
name = when (code) {
|
||||
Locale.CHINESE.language -> Locale.CHINESE.getDisplayName(Locale.CHINESE)
|
||||
NORWEGIAN_LEGACY_LANGUAGE_CODE ->
|
||||
defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
fun getCanonicalNames(): List<String> {
|
||||
var names = canonicalNamesRef.get()
|
||||
if (names == null) {
|
||||
names = getStringList(R.array.preference_language_canonical_names)
|
||||
canonicalNamesRef = SoftReference(names)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
fun getLocalizedNames(): List<String> {
|
||||
var names = localizedNamesRef.get()
|
||||
if (names == null) {
|
||||
names = getStringList(R.array.preference_language_local_names)
|
||||
localizedNamesRef = SoftReference(names)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
fun isSupportedCode(code: String?): Boolean {
|
||||
return getCodes().contains(code)
|
||||
}
|
||||
|
||||
private fun <T> defaultIndex(list: List<T>, index: Int, defaultValue: T?): T? {
|
||||
return if (inBounds(list, index)) list[index] else defaultValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches #codes for the specified language code and returns the index for use in
|
||||
* #canonicalNames and #localizedNames.
|
||||
*
|
||||
* @param code The language code to search for. The special code representing the dynamic system
|
||||
* language is null.
|
||||
* @return The index of the language code or -1 if the code is not supported.
|
||||
*/
|
||||
private fun indexOfCode(code: String?): Int {
|
||||
return getCodes().indexOf(code)
|
||||
}
|
||||
|
||||
/** @return Nonnull immutable list. */
|
||||
private fun getStringList(id: Int): List<String> {
|
||||
return getStringArray(id).toList()
|
||||
}
|
||||
|
||||
private fun inBounds(list: List<*>, index: Int): Boolean {
|
||||
return index in list.indices
|
||||
}
|
||||
|
||||
fun getStringArray(@ArrayRes id: Int): Array<String> {
|
||||
return resources.getStringArray(id)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
package fr.free.nrw.commons.location;
|
||||
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* a latitude and longitude point with accuracy information, often of a picture
|
||||
*/
|
||||
public class LatLng implements Parcelable {
|
||||
|
||||
private final double latitude;
|
||||
private final double longitude;
|
||||
private final float accuracy;
|
||||
|
||||
/**
|
||||
* Accepts latitude and longitude.
|
||||
* North and South values are cut off at 90°
|
||||
*
|
||||
* @param latitude the latitude
|
||||
* @param longitude the longitude
|
||||
* @param accuracy the accuracy
|
||||
*
|
||||
* Examples:
|
||||
* the Statue of Liberty is located at 40.69° N, 74.04° W
|
||||
* The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
|
||||
* where positive signifies north, east and negative signifies south, west.
|
||||
*/
|
||||
public LatLng(double latitude, double longitude, float accuracy) {
|
||||
if (-180.0D <= longitude && longitude < 180.0D) {
|
||||
this.longitude = longitude;
|
||||
} else {
|
||||
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
|
||||
}
|
||||
this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude));
|
||||
this.accuracy = accuracy;
|
||||
}
|
||||
/**
|
||||
* An alternate constructor for this class.
|
||||
* @param in A parcelable which contains the latitude, longitude, and accuracy
|
||||
*/
|
||||
public LatLng(Parcel in) {
|
||||
latitude = in.readDouble();
|
||||
longitude = in.readDouble();
|
||||
accuracy = in.readFloat();
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the latitude and longitude of a given non-null location
|
||||
* @param location the non-null location of the user
|
||||
* @return LatLng the Latitude and Longitude of a given location
|
||||
*/
|
||||
public static LatLng from(@NonNull Location location) {
|
||||
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a hash code for the longitude and longitude
|
||||
*/
|
||||
public int hashCode() {
|
||||
byte var1 = 1;
|
||||
long var2 = Double.doubleToLongBits(this.latitude);
|
||||
int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32);
|
||||
var2 = Double.doubleToLongBits(this.longitude);
|
||||
var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32);
|
||||
return var3;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks for equality of two LatLng objects
|
||||
* @param o the second LatLng object
|
||||
*/
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
} else if (!(o instanceof LatLng)) {
|
||||
return false;
|
||||
} else {
|
||||
LatLng var2 = (LatLng)o;
|
||||
return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a string representation of the latitude and longitude
|
||||
*/
|
||||
public String toString() {
|
||||
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the float to 4 digits and returns absolute value.
|
||||
*
|
||||
* @param coordinate A coordinate value as string.
|
||||
* @return String of the rounded number.
|
||||
*/
|
||||
private String formatCoordinate(double coordinate) {
|
||||
double roundedNumber = Math.round(coordinate * 10000d) / 10000d;
|
||||
double absoluteNumber = Math.abs(roundedNumber);
|
||||
return String.valueOf(absoluteNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "N" or "S" depending on the latitude.
|
||||
*
|
||||
* @return "N" or "S".
|
||||
*/
|
||||
private String getNorthSouth() {
|
||||
if (this.latitude < 0) {
|
||||
return "S";
|
||||
}
|
||||
|
||||
return "N";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "E" or "W" depending on the longitude.
|
||||
*
|
||||
* @return "E" or "W".
|
||||
*/
|
||||
private String getEastWest() {
|
||||
if (this.longitude >= 0 && this.longitude < 180) {
|
||||
return "E";
|
||||
}
|
||||
|
||||
return "W";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a nicely formatted coordinate string. Used e.g. in
|
||||
* the detail view.
|
||||
*
|
||||
* @return The formatted string.
|
||||
*/
|
||||
public String getPrettyCoordinateString() {
|
||||
return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
|
||||
+ formatCoordinate(this.longitude) + " " + this.getEastWest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the location accuracy in meter.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public float getAccuracy() {
|
||||
return accuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the longitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latitude in degrees.
|
||||
*
|
||||
* @return double
|
||||
*/
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public Uri getGmmIntentUri() {
|
||||
return Uri.parse("geo:" + latitude + "," + longitude + "?z=16");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeDouble(latitude);
|
||||
dest.writeDouble(longitude);
|
||||
dest.writeFloat(accuracy);
|
||||
}
|
||||
|
||||
public static final Creator<LatLng> CREATOR = new Creator<LatLng>() {
|
||||
@Override
|
||||
public LatLng createFromParcel(Parcel in) {
|
||||
return new LatLng(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LatLng[] newArray(int size) {
|
||||
return new LatLng[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
150
app/src/main/java/fr/free/nrw/commons/location/LatLng.kt
Normal file
150
app/src/main/java/fr/free/nrw/commons/location/LatLng.kt
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package fr.free.nrw.commons.location
|
||||
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.round
|
||||
|
||||
|
||||
/**
|
||||
* A latitude and longitude point with accuracy information, often of a picture.
|
||||
*/
|
||||
data class LatLng(
|
||||
var latitude: Double,
|
||||
var longitude: Double,
|
||||
val accuracy: Float
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Accepts latitude and longitude.
|
||||
* North and South values are cut off at 90°
|
||||
*
|
||||
* Examples:
|
||||
* the Statue of Liberty is located at 40.69° N, 74.04° W
|
||||
* The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
|
||||
* where positive signifies north, east and negative signifies south, west.
|
||||
*/
|
||||
init {
|
||||
val adjustedLongitude = when {
|
||||
longitude in -180.0..180.0 -> longitude
|
||||
else -> ((longitude - 180.0) % 360.0 + 360.0) % 360.0 - 180.0
|
||||
}
|
||||
latitude = max(-90.0, min(90.0, latitude))
|
||||
longitude = adjustedLongitude
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a non-null [Location] and converts it to a [LatLng].
|
||||
*/
|
||||
companion object {
|
||||
/**
|
||||
* gets the latitude and longitude of a given non-null location
|
||||
* @param location the non-null location of the user
|
||||
* @return LatLng the Latitude and Longitude of a given location
|
||||
*/
|
||||
@JvmStatic
|
||||
fun from(location: Location): LatLng {
|
||||
return LatLng(location.latitude, location.longitude, location.accuracy)
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<LatLng> = object : Parcelable.Creator<LatLng> {
|
||||
override fun createFromParcel(parcel: Parcel): LatLng {
|
||||
return LatLng(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<LatLng?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternate constructor for this class.
|
||||
* @param parcel A parcelable which contains the latitude, longitude, and accuracy
|
||||
*/
|
||||
private constructor(parcel: Parcel) : this(
|
||||
latitude = parcel.readDouble(),
|
||||
longitude = parcel.readDouble(),
|
||||
accuracy = parcel.readFloat()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a hash code for the latitude and longitude.
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
var result = 1
|
||||
val latitudeBits = latitude.toBits()
|
||||
result = 31 * result + (latitudeBits xor (latitudeBits ushr 32)).toInt()
|
||||
val longitudeBits = longitude.toBits()
|
||||
result = 31 * result + (longitudeBits xor (longitudeBits ushr 32)).toInt()
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for equality of two LatLng objects.
|
||||
* @param other the second LatLng object
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is LatLng) return false
|
||||
return latitude.toBits() == other.latitude.toBits() &&
|
||||
longitude.toBits() == other.longitude.toBits()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the latitude and longitude.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return "lat/lng: ($latitude,$longitude)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a nicely formatted coordinate string. Used e.g. in
|
||||
* the detail view.
|
||||
*
|
||||
* @return The formatted string.
|
||||
*/
|
||||
fun getPrettyCoordinateString(): String {
|
||||
return "${formatCoordinate(latitude)} ${getNorthSouth()}, " +
|
||||
"${formatCoordinate(longitude)} ${getEastWest()}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a URI for a Google Maps intent at the location.
|
||||
*/
|
||||
fun getGmmIntentUri(): Uri {
|
||||
return Uri.parse("geo:$latitude,$longitude?z=16")
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeDouble(latitude)
|
||||
parcel.writeDouble(longitude)
|
||||
parcel.writeFloat(accuracy)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
private fun formatCoordinate(coordinate: Double): String {
|
||||
val roundedNumber = round(coordinate * 10000) / 10000
|
||||
return abs(roundedNumber).toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns "N" or "S" depending on the latitude.
|
||||
*
|
||||
* @return "N" or "S".
|
||||
*/
|
||||
private fun getNorthSouth(): String = if (latitude < 0) "S" else "N"
|
||||
|
||||
/**
|
||||
* Returns "E" or "W" depending on the longitude.
|
||||
*
|
||||
* @return "E" or "W".
|
||||
*/
|
||||
private fun getEastWest(): String = if (longitude in 0.0..179.999) "E" else "W"
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
package fr.free.nrw.commons.location;
|
||||
|
||||
import android.Manifest;
|
||||
import android.Manifest.permission;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.provider.Settings;
|
||||
import android.widget.Toast;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.filepicker.Constants.RequestCodes;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
|
||||
/**
|
||||
* Helper class to handle location permissions.
|
||||
*
|
||||
* Location flow for fragments containing a map is as follows:
|
||||
* Case 1: When location permission has never been asked for or denied before
|
||||
* Check if permission is already granted or not.
|
||||
* If not already granted, ask for it (if it isn't denied twice before).
|
||||
* If now user grants permission, go to Case 3/4, else go to Case 2.
|
||||
*
|
||||
* Case 2: When location permission is just asked but has been denied
|
||||
* Shows a toast to tell the user why location permission is needed.
|
||||
* Also shows a rationale to the user, on agreeing to which, we go back to Case 1.
|
||||
* Show current location / nearby pins / nearby images according to the default location.
|
||||
*
|
||||
* Case 3: When location permission are already granted, but location services are off
|
||||
* Asks the user to turn on the location service, using a dialog.
|
||||
* If the user rejects, checks for the last known location and shows stuff using that location.
|
||||
* Also displays a toast telling the user why location should be turned on.
|
||||
*
|
||||
* Case 4: When location permission has been granted and location services are also on
|
||||
* Do whatever is required by that particular activity / fragment using current location.
|
||||
*
|
||||
*/
|
||||
public class LocationPermissionsHelper {
|
||||
|
||||
Activity activity;
|
||||
LocationServiceManager locationManager;
|
||||
LocationPermissionCallback callback;
|
||||
|
||||
public LocationPermissionsHelper(Activity activity, LocationServiceManager locationManager,
|
||||
LocationPermissionCallback callback) {
|
||||
this.activity = activity;
|
||||
this.locationManager = locationManager;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for location permission if the user agrees on attaching location with pictures and the
|
||||
* app does not have the access to location
|
||||
*
|
||||
* @param dialogTitleResource Resource id of the title of the dialog
|
||||
* @param dialogTextResource Resource id of the text of the dialog
|
||||
*/
|
||||
public void requestForLocationAccess(
|
||||
int dialogTitleResource,
|
||||
int dialogTextResource
|
||||
) {
|
||||
if (checkLocationPermission(activity)) {
|
||||
callback.onLocationPermissionGranted();
|
||||
} else {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
|
||||
permission.ACCESS_FINE_LOCATION)) {
|
||||
DialogUtil.showAlertDialog(activity, activity.getString(dialogTitleResource),
|
||||
activity.getString(dialogTextResource),
|
||||
activity.getString(android.R.string.ok),
|
||||
activity.getString(android.R.string.cancel),
|
||||
() -> {
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{permission.ACCESS_FINE_LOCATION}, 1);
|
||||
},
|
||||
() -> callback.onLocationPermissionDenied(
|
||||
activity.getString(R.string.upload_map_location_access)),
|
||||
null,
|
||||
false);
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{permission.ACCESS_FINE_LOCATION},
|
||||
RequestCodes.LOCATION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog for user to open the settings page and turn on location services
|
||||
*
|
||||
* @param activity Activity object
|
||||
* @param dialogTextResource int id of the required string resource
|
||||
*/
|
||||
public void showLocationOffDialog(Activity activity, int dialogTextResource) {
|
||||
DialogUtil
|
||||
.showAlertDialog(activity,
|
||||
activity.getString(R.string.ask_to_turn_location_on),
|
||||
activity.getString(dialogTextResource),
|
||||
activity.getString(R.string.title_app_shortcut_setting),
|
||||
activity.getString(R.string.cancel),
|
||||
() -> openLocationSettings(activity),
|
||||
() -> Toast.makeText(activity, activity.getString(dialogTextResource),
|
||||
Toast.LENGTH_LONG).show()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the location access page in settings, for user to turn on location services
|
||||
*
|
||||
* @param activity Activtiy object
|
||||
*/
|
||||
public void openLocationSettings(Activity activity) {
|
||||
final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
|
||||
final PackageManager packageManager = activity.getPackageManager();
|
||||
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
activity.startActivity(intent);
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.cannot_open_location_settings, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog for user to open the app's settings page and give location permission
|
||||
*
|
||||
* @param activity Activity object
|
||||
* @param dialogTextResource int id of the required string resource
|
||||
*/
|
||||
public void showAppSettingsDialog(Activity activity, int dialogTextResource) {
|
||||
DialogUtil
|
||||
.showAlertDialog(activity, activity.getString(R.string.location_permission_title),
|
||||
activity.getString(dialogTextResource),
|
||||
activity.getString(R.string.title_app_shortcut_setting),
|
||||
activity.getString(R.string.cancel),
|
||||
() -> openAppSettings(activity),
|
||||
() -> Toast.makeText(activity, activity.getString(dialogTextResource),
|
||||
Toast.LENGTH_LONG).show()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens detailed settings page of the app for the user to turn on location services
|
||||
*
|
||||
* @param activity Activity object
|
||||
*/
|
||||
public void openAppSettings(Activity activity) {
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if apps have access to location even after having individual access
|
||||
*
|
||||
* @return Returns true if location services are on and false otherwise
|
||||
*/
|
||||
public boolean isLocationAccessToAppsTurnedOn() {
|
||||
return (locationManager.isNetworkProviderEnabled()
|
||||
|| locationManager.isGPSProviderEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if location permission is already granted or not
|
||||
*
|
||||
* @param activity Activity object
|
||||
* @return Returns true if location permission is granted and false otherwise
|
||||
*/
|
||||
public boolean checkLocationPermission(Activity activity) {
|
||||
return PermissionUtils.hasPermission(activity,
|
||||
new String[]{Manifest.permission.ACCESS_FINE_LOCATION});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle onPermissionDenied within individual classes based on the requirements
|
||||
*/
|
||||
public interface LocationPermissionCallback {
|
||||
|
||||
void onLocationPermissionDenied(String toastMessage);
|
||||
|
||||
void onLocationPermissionGranted();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package fr.free.nrw.commons.location
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.filepicker.Constants.RequestCodes
|
||||
import fr.free.nrw.commons.utils.DialogUtil
|
||||
import fr.free.nrw.commons.utils.PermissionUtils
|
||||
|
||||
/**
|
||||
* Helper class to handle location permissions.
|
||||
*
|
||||
* Location flow for fragments containing a map is as follows:
|
||||
* Case 1: When location permission has never been asked for or denied before
|
||||
* Check if permission is already granted or not.
|
||||
* If not already granted, ask for it (if it isn't denied twice before).
|
||||
* If now user grants permission, go to Case 3/4, else go to Case 2.
|
||||
*
|
||||
* Case 2: When location permission is just asked but has been denied
|
||||
* Shows a toast to tell the user why location permission is needed.
|
||||
* Also shows a rationale to the user, on agreeing to which, we go back to Case 1.
|
||||
* Show current location / nearby pins / nearby images according to the default location.
|
||||
*
|
||||
* Case 3: When location permission are already granted, but location services are off
|
||||
* Asks the user to turn on the location service, using a dialog.
|
||||
* If the user rejects, checks for the last known location and shows stuff using that location.
|
||||
* Also displays a toast telling the user why location should be turned on.
|
||||
*
|
||||
* Case 4: When location permission has been granted and location services are also on
|
||||
* Do whatever is required by that particular activity / fragment using current location.
|
||||
*
|
||||
*/
|
||||
class LocationPermissionsHelper(
|
||||
private val activity: Activity,
|
||||
private val locationManager: LocationServiceManager,
|
||||
private val callback: LocationPermissionCallback?
|
||||
) {
|
||||
|
||||
/**
|
||||
* Ask for location permission if the user agrees on attaching location with pictures and the
|
||||
* app does not have the access to location
|
||||
*
|
||||
* @param dialogTitleResource Resource id of the title of the dialog
|
||||
* @param dialogTextResource Resource id of the text of the dialog
|
||||
*/
|
||||
fun requestForLocationAccess(
|
||||
dialogTitleResource: Int,
|
||||
dialogTextResource: Int
|
||||
) {
|
||||
if (checkLocationPermission(activity)) {
|
||||
callback?.onLocationPermissionGranted()
|
||||
} else {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
activity,
|
||||
permission.ACCESS_FINE_LOCATION
|
||||
)
|
||||
) {
|
||||
DialogUtil.showAlertDialog(
|
||||
activity,
|
||||
activity.getString(dialogTitleResource),
|
||||
activity.getString(dialogTextResource),
|
||||
activity.getString(android.R.string.ok),
|
||||
activity.getString(android.R.string.cancel),
|
||||
{
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(permission.ACCESS_FINE_LOCATION),
|
||||
1
|
||||
)
|
||||
},
|
||||
{
|
||||
callback?.onLocationPermissionDenied(
|
||||
activity.getString(R.string.upload_map_location_access)
|
||||
)
|
||||
},
|
||||
null,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(permission.ACCESS_FINE_LOCATION),
|
||||
RequestCodes.LOCATION
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog for user to open the settings page and turn on location services
|
||||
*
|
||||
* @param activity Activity object
|
||||
* @param dialogTextResource int id of the required string resource
|
||||
*/
|
||||
fun showLocationOffDialog(activity: Activity, dialogTextResource: Int) {
|
||||
DialogUtil.showAlertDialog(
|
||||
activity,
|
||||
activity.getString(R.string.ask_to_turn_location_on),
|
||||
activity.getString(dialogTextResource),
|
||||
activity.getString(R.string.title_app_shortcut_setting),
|
||||
activity.getString(R.string.cancel),
|
||||
{ openLocationSettings(activity) },
|
||||
{
|
||||
Toast.makeText(
|
||||
activity,
|
||||
activity.getString(dialogTextResource),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the location access page in settings, for user to turn on location services
|
||||
*
|
||||
* @param activity Activity object
|
||||
*/
|
||||
fun openLocationSettings(activity: Activity) {
|
||||
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
|
||||
val packageManager = activity.packageManager
|
||||
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
activity.startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.cannot_open_location_settings, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog for user to open the app's settings page and give location permission
|
||||
*
|
||||
* @param activity Activity object
|
||||
* @param dialogTextResource int id of the required string resource
|
||||
*/
|
||||
fun showAppSettingsDialog(activity: Activity, dialogTextResource: Int) {
|
||||
DialogUtil.showAlertDialog(
|
||||
activity,
|
||||
activity.getString(R.string.location_permission_title),
|
||||
activity.getString(dialogTextResource),
|
||||
activity.getString(R.string.title_app_shortcut_setting),
|
||||
activity.getString(R.string.cancel),
|
||||
{ openAppSettings(activity) },
|
||||
{
|
||||
Toast.makeText(
|
||||
activity,
|
||||
activity.getString(dialogTextResource),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens detailed settings page of the app for the user to turn on location services
|
||||
*
|
||||
* @param activity Activity object
|
||||
*/
|
||||
private fun openAppSettings(activity: Activity) {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
val uri = Uri.fromParts("package", activity.packageName, null)
|
||||
intent.data = uri
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if apps have access to location even after having individual access
|
||||
*
|
||||
* @return Returns true if location services are on and false otherwise
|
||||
*/
|
||||
fun isLocationAccessToAppsTurnedOn(): Boolean {
|
||||
return locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if location permission is already granted or not
|
||||
*
|
||||
* @param activity Activity object
|
||||
* @return Returns true if location permission is granted and false otherwise
|
||||
*/
|
||||
fun checkLocationPermission(activity: Activity): Boolean {
|
||||
return PermissionUtils.hasPermission(
|
||||
activity,
|
||||
arrayOf(permission.ACCESS_FINE_LOCATION)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle onPermissionDenied within individual classes based on the requirements
|
||||
*/
|
||||
interface LocationPermissionCallback {
|
||||
fun onLocationPermissionDenied(toastMessage: String)
|
||||
fun onLocationPermissionGranted()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,274 +0,0 @@
|
|||
package fr.free.nrw.commons.location;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LocationServiceManager implements LocationListener {
|
||||
|
||||
// Maybe these values can be improved for efficiency
|
||||
private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 10 * 100;
|
||||
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 1;
|
||||
|
||||
private LocationManager locationManager;
|
||||
private Location lastLocation;
|
||||
//private Location lastLocationDuplicate; // Will be used for nearby card view on contributions activity
|
||||
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
|
||||
private boolean isLocationManagerRegistered = false;
|
||||
private Set<Activity> locationExplanationDisplayed = new HashSet<>();
|
||||
private Context context;
|
||||
|
||||
/**
|
||||
* Constructs a new instance of LocationServiceManager.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public LocationServiceManager(Context context) {
|
||||
this.context = context;
|
||||
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
}
|
||||
|
||||
public LatLng getLastLocation() {
|
||||
if (lastLocation == null) {
|
||||
lastLocation = getLastKnownLocation();
|
||||
if(lastLocation != null) {
|
||||
return LatLng.from(lastLocation);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return LatLng.from(lastLocation);
|
||||
}
|
||||
|
||||
private Location getLastKnownLocation() {
|
||||
List<String> providers = locationManager.getProviders(true);
|
||||
Location bestLocation = null;
|
||||
for (String provider : providers) {
|
||||
Location l=null;
|
||||
if (ActivityCompat.checkSelfPermission(context, permission.ACCESS_FINE_LOCATION)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
&& ActivityCompat.checkSelfPermission(context, permission.ACCESS_COARSE_LOCATION)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
l = locationManager.getLastKnownLocation(provider);
|
||||
}
|
||||
if (l == null) {
|
||||
continue;
|
||||
}
|
||||
if (bestLocation == null
|
||||
|| l.getAccuracy() < bestLocation.getAccuracy()) {
|
||||
bestLocation = l;
|
||||
}
|
||||
}
|
||||
if (bestLocation == null) {
|
||||
return null;
|
||||
}
|
||||
return bestLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a LocationManager to listen for current location.
|
||||
*/
|
||||
public void registerLocationManager() {
|
||||
if (!isLocationManagerRegistered) {
|
||||
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
|
||||
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests location updates from the specified provider.
|
||||
*
|
||||
* @param locationProvider the location provider
|
||||
* @return true if successful
|
||||
*/
|
||||
public boolean requestLocationUpdatesFromProvider(String locationProvider) {
|
||||
try {
|
||||
// If both providers are not available
|
||||
if (locationManager == null || !(locationManager.getAllProviders().contains(locationProvider))) {
|
||||
return false;
|
||||
}
|
||||
locationManager.requestLocationUpdates(locationProvider,
|
||||
MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
|
||||
MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
|
||||
this);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
Timber.e(e, "Illegal argument exception");
|
||||
return false;
|
||||
} catch (SecurityException e) {
|
||||
Timber.e(e, "Security exception");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given location is better than the current best location.
|
||||
*
|
||||
* @param location the location to be tested
|
||||
* @param currentBestLocation the current best location
|
||||
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
|
||||
* LOCATION_SLIGHTLY_CHANGED if location changed slightly
|
||||
*/
|
||||
private LocationChangeType isBetterLocation(Location location, Location currentBestLocation) {
|
||||
|
||||
if (currentBestLocation == null) {
|
||||
// A new location is always better than no location
|
||||
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
|
||||
}
|
||||
|
||||
// Check whether the new location fix is newer or older
|
||||
long timeDelta = location.getTime() - currentBestLocation.getTime();
|
||||
boolean isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
|
||||
boolean isNewer = timeDelta > 0;
|
||||
|
||||
// Check whether the new location fix is more or less accurate
|
||||
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
|
||||
boolean isLessAccurate = accuracyDelta > 0;
|
||||
boolean isMoreAccurate = accuracyDelta < 0;
|
||||
boolean isSignificantlyLessAccurate = accuracyDelta > 200;
|
||||
|
||||
// Check if the old and new location are from the same provider
|
||||
boolean isFromSameProvider = isSameProvider(location.getProvider(),
|
||||
currentBestLocation.getProvider());
|
||||
|
||||
float[] results = new float[5];
|
||||
Location.distanceBetween(
|
||||
currentBestLocation.getLatitude(),
|
||||
currentBestLocation.getLongitude(),
|
||||
location.getLatitude(),
|
||||
location.getLongitude(),
|
||||
results);
|
||||
|
||||
// If it's been more than two minutes since the current location, use the new location
|
||||
// because the user has likely moved
|
||||
if (isSignificantlyNewer
|
||||
|| isMoreAccurate
|
||||
|| (isNewer && !isLessAccurate)
|
||||
|| (isNewer && !isSignificantlyLessAccurate && isFromSameProvider)) {
|
||||
if (results[0] < 1000) { // Means change is smaller than 1000 meter
|
||||
return LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
|
||||
} else {
|
||||
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
|
||||
}
|
||||
} else{
|
||||
return LocationChangeType.LOCATION_NOT_CHANGED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether two providers are the same
|
||||
*/
|
||||
private boolean isSameProvider(String provider1, String provider2) {
|
||||
if (provider1 == null) {
|
||||
return provider2 == null;
|
||||
}
|
||||
return provider1.equals(provider2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters location manager.
|
||||
*/
|
||||
public void unregisterLocationManager() {
|
||||
isLocationManagerRegistered = false;
|
||||
locationExplanationDisplayed.clear();
|
||||
try {
|
||||
locationManager.removeUpdates(this);
|
||||
} catch (SecurityException e) {
|
||||
Timber.e(e, "Security exception");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new listener to the list of location listeners.
|
||||
*
|
||||
* @param listener the new listener
|
||||
*/
|
||||
public void addLocationListener(LocationUpdateListener listener) {
|
||||
if (!locationListeners.contains(listener)) {
|
||||
locationListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from the list of location listeners.
|
||||
*
|
||||
* @param listener the listener to be removed
|
||||
*/
|
||||
public void removeLocationListener(LocationUpdateListener listener) {
|
||||
locationListeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
Timber.d("on location changed");
|
||||
if (isBetterLocation(location, lastLocation)
|
||||
.equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) {
|
||||
lastLocation = location;
|
||||
//lastLocationDuplicate = location;
|
||||
for (LocationUpdateListener listener : locationListeners) {
|
||||
listener.onLocationChangedSignificantly(LatLng.from(lastLocation));
|
||||
}
|
||||
} else if (location.distanceTo(lastLocation) >= 500) {
|
||||
// Update nearby notification card at every 500 meters.
|
||||
for (LocationUpdateListener listener : locationListeners) {
|
||||
listener.onLocationChangedMedium(LatLng.from(lastLocation));
|
||||
}
|
||||
}
|
||||
|
||||
else if (isBetterLocation(location, lastLocation)
|
||||
.equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
|
||||
lastLocation = location;
|
||||
//lastLocationDuplicate = location;
|
||||
for (LocationUpdateListener listener : locationListeners) {
|
||||
listener.onLocationChangedSlightly(LatLng.from(lastLocation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
Timber.d("%s's status changed to %d", provider, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
Timber.d("Provider %s enabled", provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
Timber.d("Provider %s disabled", provider);
|
||||
}
|
||||
|
||||
public boolean isNetworkProviderEnabled() {
|
||||
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
|
||||
}
|
||||
|
||||
public boolean isGPSProviderEnabled() {
|
||||
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
|
||||
}
|
||||
|
||||
public enum LocationChangeType{
|
||||
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
|
||||
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving
|
||||
LOCATION_MEDIUM_CHANGED, //Between slight and significant changes, will be used for nearby card view updates.
|
||||
LOCATION_NOT_CHANGED,
|
||||
PERMISSION_JUST_GRANTED,
|
||||
MAP_UPDATED,
|
||||
SEARCH_CUSTOM_AREA,
|
||||
CUSTOM_QUERY
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
package fr.free.nrw.commons.location
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.ActivityCompat
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
|
||||
class LocationServiceManager(private val context: Context) : LocationListener {
|
||||
|
||||
companion object {
|
||||
// Maybe these values can be improved for efficiency
|
||||
private const val MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 10 * 100L
|
||||
private const val MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 1f
|
||||
}
|
||||
|
||||
private val locationManager: LocationManager =
|
||||
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private var lastLocationVar: Location? = null
|
||||
private val locationListeners = CopyOnWriteArrayList<LocationUpdateListener>()
|
||||
private var isLocationManagerRegistered = false
|
||||
private val locationExplanationDisplayed = mutableSetOf<Activity>()
|
||||
|
||||
/**
|
||||
* Constructs a new instance of LocationServiceManager.
|
||||
*
|
||||
*/
|
||||
fun getLastLocation(): LatLng? {
|
||||
if (lastLocationVar == null) {
|
||||
lastLocationVar = getLastKnownLocation()
|
||||
return lastLocationVar?.let { LatLng.from(it) }
|
||||
}
|
||||
return LatLng.from(lastLocationVar!!)
|
||||
}
|
||||
|
||||
private fun getLastKnownLocation(): Location? {
|
||||
val providers = locationManager.getProviders(true)
|
||||
var bestLocation: Location? = null
|
||||
for (provider in providers) {
|
||||
val location: Location? = if (
|
||||
ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
==
|
||||
PackageManager.PERMISSION_GRANTED &&
|
||||
ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
locationManager.getLastKnownLocation(provider)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (
|
||||
location != null
|
||||
&&
|
||||
(bestLocation == null || location.accuracy < bestLocation.accuracy)
|
||||
) {
|
||||
bestLocation = location
|
||||
}
|
||||
}
|
||||
return bestLocation
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a LocationManager to listen for current location.
|
||||
*/
|
||||
fun registerLocationManager() {
|
||||
if (!isLocationManagerRegistered) {
|
||||
isLocationManagerRegistered =
|
||||
requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) &&
|
||||
requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests location updates from the specified provider.
|
||||
*
|
||||
* @param locationProvider the location provider
|
||||
* @return true if successful
|
||||
*/
|
||||
fun requestLocationUpdatesFromProvider(locationProvider: String): Boolean {
|
||||
return try {
|
||||
if (locationManager.allProviders.contains(locationProvider)) {
|
||||
locationManager.requestLocationUpdates(
|
||||
locationProvider,
|
||||
MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
|
||||
MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
|
||||
this
|
||||
)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e, "Illegal argument exception")
|
||||
false
|
||||
} catch (e: SecurityException) {
|
||||
Timber.e(e, "Security exception")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given location is better than the current best location.
|
||||
*
|
||||
* @param location the location to be tested
|
||||
* @param currentBestLocation the current best location
|
||||
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
|
||||
* LOCATION_SLIGHTLY_CHANGED if location changed slightly
|
||||
*/
|
||||
private fun isBetterLocation(location: Location, currentBestLocation: Location?): LocationChangeType {
|
||||
if (currentBestLocation == null) {
|
||||
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED
|
||||
}
|
||||
|
||||
val timeDelta = location.time - currentBestLocation.time
|
||||
val isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS
|
||||
val isNewer = timeDelta > 0
|
||||
val accuracyDelta = (location.accuracy - currentBestLocation.accuracy).toInt()
|
||||
val isMoreAccurate = accuracyDelta < 0
|
||||
val isSignificantlyLessAccurate = accuracyDelta > 200
|
||||
val isFromSameProvider = isSameProvider(location.provider, currentBestLocation.provider)
|
||||
|
||||
val results = FloatArray(5)
|
||||
Location.distanceBetween(
|
||||
currentBestLocation.latitude, currentBestLocation.longitude,
|
||||
location.latitude, location.longitude,
|
||||
results
|
||||
)
|
||||
|
||||
return when {
|
||||
isSignificantlyNewer
|
||||
||
|
||||
isMoreAccurate
|
||||
||
|
||||
(isNewer && !isSignificantlyLessAccurate && isFromSameProvider) -> {
|
||||
if (results[0] < 1000) LocationChangeType.LOCATION_SLIGHTLY_CHANGED
|
||||
else LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED
|
||||
}
|
||||
else -> LocationChangeType.LOCATION_NOT_CHANGED
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether two providers are the same
|
||||
*/
|
||||
private fun isSameProvider(provider1: String?, provider2: String?): Boolean {
|
||||
return provider1 == provider2
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters location manager.
|
||||
*/
|
||||
fun unregisterLocationManager() {
|
||||
isLocationManagerRegistered = false
|
||||
locationExplanationDisplayed.clear()
|
||||
try {
|
||||
locationManager.removeUpdates(this)
|
||||
} catch (e: SecurityException) {
|
||||
Timber.e(e, "Security exception")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new listener to the list of location listeners.
|
||||
*
|
||||
* @param listener the new listener
|
||||
*/
|
||||
fun addLocationListener(listener: LocationUpdateListener) {
|
||||
if (!locationListeners.contains(listener)) {
|
||||
locationListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from the list of location listeners.
|
||||
*
|
||||
* @param listener the listener to be removed
|
||||
*/
|
||||
fun removeLocationListener(listener: LocationUpdateListener) {
|
||||
locationListeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
Timber.d("on location changed")
|
||||
val changeType = isBetterLocation(location, lastLocationVar)
|
||||
if (changeType == LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) {
|
||||
lastLocationVar = location
|
||||
locationListeners.forEach { it.onLocationChangedSignificantly(LatLng.from(location)) }
|
||||
} else if (lastLocationVar?.let { location.distanceTo(it) }!! >= 500) {
|
||||
locationListeners.forEach { it.onLocationChangedMedium(LatLng.from(location)) }
|
||||
} else if (changeType == LocationChangeType.LOCATION_SLIGHTLY_CHANGED) {
|
||||
lastLocationVar = location
|
||||
locationListeners.forEach { it.onLocationChangedSlightly(LatLng.from(location)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java", ReplaceWith(
|
||||
"Timber.d(\"%s's status changed to %d\", provider, status)",
|
||||
"timber.log.Timber"
|
||||
)
|
||||
)
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
|
||||
Timber.d("%s's status changed to %d", provider, status)
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun onProviderEnabled(provider: String) {
|
||||
Timber.d("Provider %s enabled", provider)
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
Timber.d("Provider %s disabled", provider)
|
||||
}
|
||||
|
||||
fun isNetworkProviderEnabled(): Boolean {
|
||||
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
}
|
||||
|
||||
fun isGPSProviderEnabled(): Boolean {
|
||||
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|
||||
}
|
||||
|
||||
enum class LocationChangeType {
|
||||
LOCATION_SIGNIFICANTLY_CHANGED,
|
||||
LOCATION_SLIGHTLY_CHANGED,
|
||||
LOCATION_MEDIUM_CHANGED,
|
||||
LOCATION_NOT_CHANGED,
|
||||
PERMISSION_JUST_GRANTED,
|
||||
MAP_UPDATED,
|
||||
SEARCH_CUSTOM_AREA,
|
||||
CUSTOM_QUERY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package fr.free.nrw.commons.location;
|
||||
|
||||
public interface LocationUpdateListener {
|
||||
void onLocationChangedSignificantly(LatLng latLng); // Will be used to update all nearby markers on the map
|
||||
void onLocationChangedSlightly(LatLng latLng); // Will be used to track users motion
|
||||
void onLocationChangedMedium(LatLng latLng); // Will be used updating nearby card view notification
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package fr.free.nrw.commons.location
|
||||
|
||||
interface LocationUpdateListener {
|
||||
// Will be used to update all nearby markers on the map
|
||||
fun onLocationChangedSignificantly(latLng: LatLng)
|
||||
|
||||
// Will be used to track users motion
|
||||
fun onLocationChangedSlightly(latLng: LatLng)
|
||||
|
||||
// Will be used updating nearby card view notification
|
||||
fun onLocationChangedMedium(latLng: LatLng)
|
||||
}
|
||||
|
|
@ -42,8 +42,8 @@ class LanguagesAdapter constructor(
|
|||
AppLanguageLookUpTable(context)
|
||||
|
||||
init {
|
||||
languageNamesList = language.localizedNames
|
||||
languageCodesList = language.codes
|
||||
languageNamesList = language.getLocalizedNames()
|
||||
languageCodesList = language.getCodes()
|
||||
}
|
||||
|
||||
private val filter = LanguageFilter()
|
||||
|
|
@ -117,7 +117,7 @@ class LanguagesAdapter constructor(
|
|||
*/
|
||||
fun getIndexOfUserDefaultLocale(context: Context): Int {
|
||||
val userLanguageCode = context.locale?.language ?: return DEFAULT_INDEX
|
||||
return language.codes.indexOf(userLanguageCode).takeIf { it >= 0 } ?: DEFAULT_INDEX
|
||||
return language.getCodes().indexOf(userLanguageCode).takeIf { it >= 0 } ?: DEFAULT_INDEX
|
||||
}
|
||||
|
||||
fun getIndexOfLanguageCode(languageCode: String): Int = languageCodesList.indexOf(languageCode)
|
||||
|
|
@ -128,17 +128,17 @@ class LanguagesAdapter constructor(
|
|||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val filterResults = FilterResults()
|
||||
val temp: LinkedHashMap<String, String> = LinkedHashMap()
|
||||
if (constraint != null && language.localizedNames != null) {
|
||||
val length: Int = language.localizedNames.size
|
||||
if (constraint != null) {
|
||||
val length: Int = language.getLocalizedNames().size
|
||||
var i = 0
|
||||
while (i < length) {
|
||||
val key: String = language.codes[i]
|
||||
val value: String = language.localizedNames[i]
|
||||
val key: String = language.getCodes()[i]
|
||||
val value: String = language.getLocalizedNames()[i]
|
||||
val defaultlanguagecode = getIndexOfUserDefaultLocale(context)
|
||||
if (value.contains(constraint, true) ||
|
||||
Locale(key)
|
||||
.getDisplayName(
|
||||
Locale(language.codes[defaultlanguagecode]),
|
||||
Locale(language.getCodes()[defaultlanguagecode]),
|
||||
).contains(constraint, true)
|
||||
) {
|
||||
temp[key] = value
|
||||
|
|
|
|||
|
|
@ -62,5 +62,5 @@ class LatLngTests {
|
|||
private fun assertPrettyCoordinateString(
|
||||
expected: String,
|
||||
place: LatLng,
|
||||
) = assertEquals(expected, place.prettyCoordinateString)
|
||||
) = assertEquals(expected, place.getPrettyCoordinateString())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,11 +248,11 @@ class MediaDetailFragmentUnitTests {
|
|||
@Throws(Exception::class)
|
||||
fun testOnUpdateCoordinatesClickedCurrentLocationNull() {
|
||||
`when`(media.coordinates).thenReturn(null)
|
||||
`when`(locationManager.lastLocation).thenReturn(null)
|
||||
`when`(locationManager.getLastLocation()).thenReturn(null)
|
||||
`when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297")
|
||||
fragment.onUpdateCoordinatesClicked()
|
||||
Mockito.verify(media, Mockito.times(1)).coordinates
|
||||
Mockito.verify(locationManager, Mockito.times(1)).lastLocation
|
||||
Mockito.verify(locationManager, Mockito.times(1)).getLastLocation()
|
||||
val shadowActivity: ShadowActivity = shadowOf(activity)
|
||||
val startedIntent = shadowActivity.nextStartedActivity
|
||||
val shadowIntent: ShadowIntent = shadowOf(startedIntent)
|
||||
|
|
@ -276,11 +276,11 @@ class MediaDetailFragmentUnitTests {
|
|||
@Throws(Exception::class)
|
||||
fun testOnUpdateCoordinatesClickedCurrentLocationNotNull() {
|
||||
`when`(media.coordinates).thenReturn(null)
|
||||
`when`(locationManager.lastLocation).thenReturn(LatLng(-0.000001, -0.999999, 0f))
|
||||
`when`(locationManager.getLastLocation()).thenReturn(LatLng(-0.000001, -0.999999, 0f))
|
||||
`when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297")
|
||||
|
||||
fragment.onUpdateCoordinatesClicked()
|
||||
Mockito.verify(locationManager, Mockito.times(3)).lastLocation
|
||||
Mockito.verify(locationManager, Mockito.times(3)).getLastLocation()
|
||||
val shadowActivity: ShadowActivity = shadowOf(activity)
|
||||
val startedIntent = shadowActivity.nextStartedActivity
|
||||
val shadowIntent: ShadowIntent = shadowOf(startedIntent)
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ class LanguagesAdapterTest {
|
|||
.from(context)
|
||||
.inflate(R.layout.row_item_languages_spinner, null) as View
|
||||
|
||||
languageNamesList = language.localizedNames
|
||||
languageCodesList = language.codes
|
||||
languageNamesList = language.getLocalizedNames()
|
||||
languageCodesList = language.getCodes()
|
||||
|
||||
languagesAdapter = LanguagesAdapter(context, selectedLanguages)
|
||||
}
|
||||
|
|
@ -124,12 +124,12 @@ class LanguagesAdapterTest {
|
|||
var i = 0
|
||||
var s = 0
|
||||
while (i < length) {
|
||||
val key: String = language.codes[i]
|
||||
val value: String = language.localizedNames[i]
|
||||
val key: String = language.getCodes()[i]
|
||||
val value: String = language.getLocalizedNames()[i]
|
||||
if (value.contains(constraint, true) ||
|
||||
Locale(key)
|
||||
.getDisplayName(
|
||||
Locale(language.codes[defaultlanguagecode!!]),
|
||||
Locale(language.getCodes()[defaultlanguagecode!!]),
|
||||
).contains(constraint, true)
|
||||
) {
|
||||
s++
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue