@ -0,0 +1,85 @@ | |||
using eShopOnContainers.Core.Models.Location; | |||
using System; | |||
namespace eShopOnContainers.Droid.Extensions | |||
{ | |||
public static class LocationExtensions | |||
{ | |||
static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | |||
static int TwoMinutes = 120000; | |||
internal static Position ToPosition(this Android.Locations.Location location) | |||
{ | |||
var p = new Position(); | |||
if (location.HasAccuracy) | |||
p.Accuracy = location.Accuracy; | |||
if (location.HasAltitude) | |||
p.Altitude = location.Altitude; | |||
if (location.HasBearing) | |||
p.Heading = location.Bearing; | |||
if (location.HasSpeed) | |||
p.Speed = location.Speed; | |||
p.Longitude = location.Longitude; | |||
p.Latitude = location.Latitude; | |||
p.Timestamp = location.GetTimestamp(); | |||
return p; | |||
} | |||
internal static DateTimeOffset GetTimestamp(this Android.Locations.Location location) | |||
{ | |||
try | |||
{ | |||
return new DateTimeOffset(Epoch.AddMilliseconds(location.Time)); | |||
} | |||
catch (Exception e) | |||
{ | |||
return new DateTimeOffset(Epoch); | |||
} | |||
} | |||
internal static bool IsBetterLocation(this Android.Locations.Location location, Android.Locations.Location bestLocation) | |||
{ | |||
if (bestLocation == null) | |||
return true; | |||
var timeDelta = location.Time - bestLocation.Time; | |||
var isSignificantlyNewer = timeDelta > TwoMinutes; | |||
var isSignificantlyOlder = timeDelta < -TwoMinutes; | |||
var isNewer = timeDelta > 0; | |||
if (isSignificantlyNewer) | |||
return true; | |||
if (isSignificantlyOlder) | |||
return false; | |||
var accuracyDelta = (int)(location.Accuracy - bestLocation.Accuracy); | |||
var isLessAccurate = accuracyDelta > 0; | |||
var isMoreAccurate = accuracyDelta < 0; | |||
var isSignificantlyLessAccurage = accuracyDelta > 200; | |||
var isFromSameProvider = IsSameProvider(location.Provider, bestLocation.Provider); | |||
if (isMoreAccurate) | |||
return true; | |||
if (isNewer && !isLessAccurate) | |||
return true; | |||
if (isNewer && !isSignificantlyLessAccurage && isFromSameProvider) | |||
return true; | |||
return false; | |||
} | |||
internal static bool IsSameProvider(string provider1, string provider2) | |||
{ | |||
if (provider1 == null) | |||
return provider2 == null; | |||
return provider1.Equals(provider2); | |||
} | |||
} | |||
} |
@ -0,0 +1,112 @@ | |||
using System; | |||
using System.Threading.Tasks; | |||
using Android.Locations; | |||
using Android.OS; | |||
using System.Threading; | |||
using System.Collections.Generic; | |||
using Android.Runtime; | |||
using eShopOnContainers.Core.Models.Location; | |||
using eShopOnContainers.Droid.Extensions; | |||
namespace eShopOnContainers.Droid.Services | |||
{ | |||
public class GeolocationSingleListener : Java.Lang.Object, ILocationListener | |||
{ | |||
readonly object _locationSync = new object(); | |||
readonly Action _finishedCallback; | |||
readonly float _desiredAccuracy; | |||
readonly Timer _timer; | |||
readonly TaskCompletionSource<Position> _tcs = new TaskCompletionSource<Position>(); | |||
HashSet<string> _activeProviders = new HashSet<string>(); | |||
Android.Locations.Location _bestLocation; | |||
public Task<Position> Task => _tcs.Task; | |||
public GeolocationSingleListener(LocationManager manager, float desiredAccuracy, int timeout, IEnumerable<string> activeProviders, Action finishedCallback) | |||
{ | |||
_desiredAccuracy = desiredAccuracy; | |||
_finishedCallback = finishedCallback; | |||
_activeProviders = new HashSet<string>(activeProviders); | |||
foreach (var provider in activeProviders) | |||
{ | |||
var location = manager.GetLastKnownLocation(provider); | |||
if (location != null && location.IsBetterLocation(_bestLocation)) | |||
_bestLocation = location; | |||
} | |||
if (timeout != Timeout.Infinite) | |||
_timer = new Timer(TimesUp, null, timeout, 0); | |||
} | |||
public void Cancel() => _tcs.TrySetCanceled(); | |||
public void OnLocationChanged(Android.Locations.Location location) | |||
{ | |||
if (location.Accuracy <= _desiredAccuracy) | |||
{ | |||
Finish(location); | |||
return; | |||
} | |||
lock (_locationSync) | |||
{ | |||
if (location.IsBetterLocation(_bestLocation)) | |||
_bestLocation = location; | |||
} | |||
} | |||
public void OnProviderDisabled(string provider) | |||
{ | |||
lock (_activeProviders) | |||
{ | |||
if (_activeProviders.Remove(provider) && _activeProviders.Count == 0) | |||
_tcs.TrySetException(new GeolocationException(GeolocationError.PositionUnavailable)); | |||
} | |||
} | |||
public void OnProviderEnabled(string provider) | |||
{ | |||
lock (_activeProviders) | |||
_activeProviders.Add(provider); | |||
} | |||
public void OnStatusChanged(string provider, [GeneratedEnum] Availability status, Bundle extras) | |||
{ | |||
switch (status) | |||
{ | |||
case Availability.Available: | |||
OnProviderEnabled(provider); | |||
break; | |||
case Availability.OutOfService: | |||
OnProviderDisabled(provider); | |||
break; | |||
} | |||
} | |||
void TimesUp(object state) | |||
{ | |||
lock (_locationSync) | |||
{ | |||
if (_bestLocation == null) | |||
{ | |||
if (_tcs.TrySetCanceled()) | |||
_finishedCallback?.Invoke(); | |||
} | |||
else | |||
{ | |||
Finish(_bestLocation); | |||
} | |||
} | |||
} | |||
void Finish(Android.Locations.Location location) | |||
{ | |||
_finishedCallback?.Invoke(); | |||
_tcs.TrySetResult(location.ToPosition()); | |||
} | |||
} | |||
} |
@ -0,0 +1,155 @@ | |||
using Android.App; | |||
using Android.Content; | |||
using eShopOnContainers.Droid.Services; | |||
using System; | |||
using eShopOnContainers.Core.Services.Location; | |||
using eShopOnContainers.Core.Models.Location; | |||
using eShopOnContainers.Core.Services.Permissions; | |||
using eShopOnContainers.Core.Models.Permissions; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using Android.Locations; | |||
using System.Linq; | |||
using System.Collections.Generic; | |||
using Android.OS; | |||
[assembly: Xamarin.Forms.Dependency(typeof(LocationServiceImplementation))] | |||
namespace eShopOnContainers.Droid.Services | |||
{ | |||
public class LocationServiceImplementation : ILocationServiceImplementation | |||
{ | |||
#region Internal Implementation | |||
LocationManager _locationManager; | |||
GeolocationSingleListener _singleListener = null; | |||
string[] Providers => Manager.GetProviders(enabledOnly: false).ToArray(); | |||
string[] IgnoredProviders => new string[] { LocationManager.PassiveProvider, "local_database" }; | |||
public static string[] ProvidersToUse { get; set; } = new string[] { }; | |||
LocationManager Manager | |||
{ | |||
get | |||
{ | |||
if (_locationManager == null) | |||
_locationManager = (LocationManager)Application.Context.GetSystemService(Context.LocationService); | |||
return _locationManager; | |||
} | |||
} | |||
public LocationServiceImplementation() | |||
{ | |||
DesiredAccuracy = 100; | |||
} | |||
async Task<bool> CheckPermissionsAsync() | |||
{ | |||
IPermissionsService permissionsService = new PermissionsService(); | |||
var status = await permissionsService.CheckPermissionStatusAsync(Permission.Location); | |||
if (status != PermissionStatus.Granted) | |||
{ | |||
Console.WriteLine("Currently do not have Location permissions, requesting permissions"); | |||
var request = await permissionsService.RequestPermissionsAsync(Permission.Location); | |||
if (request[Permission.Location] != PermissionStatus.Granted) | |||
{ | |||
Console.WriteLine("Location permission denied."); | |||
return false; | |||
} | |||
} | |||
return true; | |||
} | |||
#endregion | |||
#region ILocationServiceImplementation | |||
public double DesiredAccuracy { get; set; } | |||
public bool IsGeolocationAvailable => Providers.Length > 0; | |||
public bool IsGeolocationEnabled => Providers.Any(p => !IgnoredProviders.Contains(p) && Manager.IsProviderEnabled(p)); | |||
public async Task<Position> GetPositionAsync(TimeSpan? timeout = null, CancellationToken? cancelToken = null, bool includeHeading = false) | |||
{ | |||
var timeoutMilliseconds = timeout.HasValue ? (int)timeout.Value.TotalMilliseconds : Timeout.Infinite; | |||
if (timeoutMilliseconds <= 0 && timeoutMilliseconds != Timeout.Infinite) | |||
throw new ArgumentOutOfRangeException(nameof(timeout), "Timeout must be greater than or equal to 0"); | |||
if (!cancelToken.HasValue) | |||
cancelToken = CancellationToken.None; | |||
var hasPermission = await CheckPermissionsAsync(); | |||
if (!hasPermission) | |||
throw new GeolocationException(GeolocationError.Unauthorized); | |||
var tcs = new TaskCompletionSource<Position>(); | |||
var providers = new List<string>(); | |||
if (ProvidersToUse == null || ProvidersToUse.Length == 0) | |||
providers.AddRange(Providers); | |||
else | |||
{ | |||
foreach (var provider in Providers) | |||
{ | |||
if (ProvidersToUse?.Contains(provider) ?? false) | |||
continue; | |||
providers.Add(provider); | |||
} | |||
} | |||
void SingleListenerFinishCallback() | |||
{ | |||
if (_singleListener == null) | |||
return; | |||
for (int i = 0; i < providers.Count; ++i) | |||
Manager.RemoveUpdates(_singleListener); | |||
} | |||
_singleListener = new GeolocationSingleListener(Manager, (float)DesiredAccuracy, timeoutMilliseconds, providers.Where(Manager.IsProviderEnabled), finishedCallback: SingleListenerFinishCallback); | |||
if (cancelToken != CancellationToken.None) | |||
{ | |||
cancelToken.Value.Register(() => | |||
{ | |||
_singleListener.Cancel(); | |||
for (int i = 0; i < providers.Count; ++i) | |||
Manager.RemoveUpdates(_singleListener); | |||
}, true); | |||
} | |||
try | |||
{ | |||
var looper = Looper.MyLooper() ?? Looper.MainLooper; | |||
int enabled = 0; | |||
for (var i = 0; i < providers.Count; ++i) | |||
{ | |||
if (Manager.IsProviderEnabled(providers[i])) | |||
enabled++; | |||
Manager.RequestLocationUpdates(providers[i], 0, 0, _singleListener, looper); | |||
} | |||
if (enabled == 0) | |||
{ | |||
for (int i = 0; i < providers.Count; ++i) | |||
Manager.RemoveUpdates(_singleListener); | |||
tcs.SetException(new GeolocationException(GeolocationError.PositionUnavailable)); | |||
return await tcs.Task; | |||
} | |||
} | |||
catch (Java.Lang.SecurityException ex) | |||
{ | |||
tcs.SetException(new GeolocationException(GeolocationError.Unauthorized, ex)); | |||
return await tcs.Task; | |||
} | |||
return await _singleListener.Task; | |||
} | |||
#endregion | |||
} | |||
} |