Removed geolocation plugin from mobile appspull/522/head
@ -0,0 +1,8 @@ | |||
namespace eShopOnContainers.Core.Models.Location | |||
{ | |||
public enum GeolocationError | |||
{ | |||
PositionUnavailable, | |||
Unauthorized | |||
} | |||
} |
@ -0,0 +1,27 @@ | |||
using System; | |||
namespace eShopOnContainers.Core.Models.Location | |||
{ | |||
public class GeolocationException : Exception | |||
{ | |||
public GeolocationError Error { get; private set; } | |||
public GeolocationException(GeolocationError error) | |||
: base("A geolocation error occured: " + error) | |||
{ | |||
if (!Enum.IsDefined(typeof(GeolocationError), error)) | |||
throw new ArgumentException("error is not a valid GelocationError member", "error"); | |||
Error = error; | |||
} | |||
public GeolocationException(GeolocationError error, Exception innerException) | |||
: base("A geolocation error occured: " + error, innerException) | |||
{ | |||
if (!Enum.IsDefined(typeof(GeolocationError), error)) | |||
throw new ArgumentException("error is not a valid GelocationError member", "error"); | |||
Error = error; | |||
} | |||
} | |||
} |
@ -0,0 +1,43 @@ | |||
using System; | |||
namespace eShopOnContainers.Core.Models.Location | |||
{ | |||
public class Position | |||
{ | |||
public DateTimeOffset Timestamp { get; set; } | |||
public double Latitude { get; set; } | |||
public double Longitude { get; set; } | |||
public double Altitude { get; set; } | |||
public double Accuracy { get; set; } | |||
public double AltitudeAccuracy { get; set; } | |||
public double Heading { get; set; } | |||
public double Speed { get; set; } | |||
public Position() | |||
{ | |||
} | |||
public Position(double latitude, double longitude) | |||
{ | |||
Timestamp = DateTimeOffset.UtcNow; | |||
Latitude = latitude; | |||
Longitude = longitude; | |||
} | |||
public Position(Position position) | |||
{ | |||
if (position == null) | |||
throw new ArgumentNullException("position"); | |||
Timestamp = position.Timestamp; | |||
Latitude = position.Latitude; | |||
Longitude = position.Longitude; | |||
Altitude = position.Altitude; | |||
AltitudeAccuracy = position.AltitudeAccuracy; | |||
Accuracy = position.Accuracy; | |||
Heading = position.Heading; | |||
Speed = position.Speed; | |||
} | |||
} | |||
} |
@ -0,0 +1,10 @@ | |||
namespace eShopOnContainers.Core.Models.Permissions | |||
{ | |||
public enum Permission | |||
{ | |||
Unknown, | |||
Location, | |||
LocationAlways, | |||
LocationWhenInUse | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
namespace eShopOnContainers.Core.Models.Permissions | |||
{ | |||
public enum PermissionStatus | |||
{ | |||
Denied, | |||
Disabled, | |||
Granted, | |||
Restricted, | |||
Unknown | |||
} | |||
} |
@ -0,0 +1,16 @@ | |||
using eShopOnContainers.Core.Models.Location; | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace eShopOnContainers.Core.Services.Location | |||
{ | |||
public interface ILocationServiceImplementation | |||
{ | |||
double DesiredAccuracy { get; set; } | |||
bool IsGeolocationAvailable { get; } | |||
bool IsGeolocationEnabled { get; } | |||
Task<Position> GetPositionAsync(TimeSpan? timeout = null, CancellationToken? token = null); | |||
} | |||
} |
@ -0,0 +1,12 @@ | |||
using System.Collections.Generic; | |||
using System.Threading.Tasks; | |||
using eShopOnContainers.Core.Models.Permissions; | |||
namespace eShopOnContainers.Core.Services.Permissions | |||
{ | |||
public interface IPermissionsService | |||
{ | |||
Task<PermissionStatus> CheckPermissionStatusAsync(Permission permission); | |||
Task<Dictionary<Permission, PermissionStatus>> RequestPermissionsAsync(params Permission[] permissions); | |||
} | |||
} |
@ -0,0 +1,84 @@ | |||
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,110 @@ | |||
using Android.Locations; | |||
using Android.OS; | |||
using Android.Runtime; | |||
using eShopOnContainers.Core.Models.Location; | |||
using eShopOnContainers.Droid.Extensions; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
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,152 @@ | |||
using Android.App; | |||
using Android.Content; | |||
using Android.Locations; | |||
using Android.OS; | |||
using eShopOnContainers.Core.Models.Location; | |||
using eShopOnContainers.Core.Models.Permissions; | |||
using eShopOnContainers.Core.Services.Location; | |||
using eShopOnContainers.Droid.Services; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
[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() | |||
{ | |||
var status = await PermissionsService.Current.Value.CheckPermissionStatusAsync(Permission.Location); | |||
if (status != PermissionStatus.Granted) | |||
{ | |||
Console.WriteLine("Currently do not have Location permissions, requesting permissions."); | |||
var request = await PermissionsService.Current.Value.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) | |||
{ | |||
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 | |||
} | |||
} |
@ -0,0 +1,242 @@ | |||
using Android; | |||
using Android.App; | |||
using Android.Support.V4.App; | |||
using Android.Support.V4.Content; | |||
using eShopOnContainers.Core.Models.Permissions; | |||
using eShopOnContainers.Core.Services.Permissions; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Diagnostics; | |||
using System.Linq; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace eShopOnContainers.Droid.Services | |||
{ | |||
public class PermissionsService : IPermissionsService | |||
{ | |||
const int _permissionCode = 25; | |||
object _locker = new object(); | |||
TaskCompletionSource<Dictionary<Permission, PermissionStatus>> _tcs; | |||
Dictionary<Permission, PermissionStatus> _results; | |||
IList<string> _requestedPermissions; | |||
public static Lazy<IPermissionsService> Current = new Lazy<IPermissionsService>(() => new PermissionsService(), LazyThreadSafetyMode.PublicationOnly); | |||
#region Internal Implementation | |||
List<string> GetManifestNames(Permission permission) | |||
{ | |||
var permissionNames = new List<string>(); | |||
switch (permission) | |||
{ | |||
case Permission.LocationAlways: | |||
case Permission.LocationWhenInUse: | |||
case Permission.Location: | |||
{ | |||
if (HasPermissionInManifest(Manifest.Permission.AccessCoarseLocation)) | |||
permissionNames.Add(Manifest.Permission.AccessCoarseLocation); | |||
if (HasPermissionInManifest(Manifest.Permission.AccessFineLocation)) | |||
permissionNames.Add(Manifest.Permission.AccessFineLocation); | |||
} | |||
break; | |||
} | |||
return permissionNames; | |||
} | |||
bool HasPermissionInManifest(string permission) | |||
{ | |||
try | |||
{ | |||
if (_requestedPermissions != null) | |||
return _requestedPermissions.Any(r => r.Equals(permission, StringComparison.InvariantCultureIgnoreCase)); | |||
// Try to use current activity else application context | |||
var context = MainApplication.CurrentContext ?? Application.Context; | |||
if (context == null) | |||
{ | |||
Debug.WriteLine("Unable to detect current Activity or Application Context."); | |||
return false; | |||
} | |||
var info = context.PackageManager.GetPackageInfo(context.PackageName, Android.Content.PM.PackageInfoFlags.Permissions); | |||
if (info == null) | |||
{ | |||
Debug.WriteLine("Unable to get package info, will not be able to determine permissions to request."); | |||
return false; | |||
} | |||
_requestedPermissions = info.RequestedPermissions; | |||
if (_requestedPermissions == null) | |||
{ | |||
Debug.WriteLine("There are no requested permissions, please check to ensure you have marked the permissions that you want to request."); | |||
return false; | |||
} | |||
return _requestedPermissions.Any(r => r.Equals(permission, StringComparison.InvariantCultureIgnoreCase)); | |||
} | |||
catch (Exception ex) | |||
{ | |||
Console.Write("Unable to check manifest for permission: " + ex); | |||
} | |||
return false; | |||
} | |||
Permission GetPermissionForManifestName(string permission) | |||
{ | |||
switch (permission) | |||
{ | |||
case Manifest.Permission.AccessCoarseLocation: | |||
case Manifest.Permission.AccessFineLocation: | |||
return Permission.Location; | |||
} | |||
return Permission.Unknown; | |||
} | |||
public void OnRequestPermissionResult(int requestCode, string[] permissions, Android.Content.PM.Permission[] grantResults) | |||
{ | |||
if (requestCode != _permissionCode) | |||
return; | |||
if (_tcs == null) | |||
return; | |||
for (var i = 0; i < permissions.Length; i++) | |||
{ | |||
if (_tcs.Task.Status == TaskStatus.Canceled) | |||
return; | |||
var permission = GetPermissionForManifestName(permissions[i]); | |||
if (permission == Permission.Unknown) | |||
continue; | |||
lock (_locker) | |||
{ | |||
if (permission == Permission.Location) | |||
{ | |||
if (!_results.ContainsKey(Permission.LocationWhenInUse)) | |||
_results.Add(Permission.LocationWhenInUse, grantResults[i] == Android.Content.PM.Permission.Granted ? PermissionStatus.Granted : PermissionStatus.Denied); | |||
} | |||
if (!_results.ContainsKey(permission)) | |||
_results.Add(permission, grantResults[i] == Android.Content.PM.Permission.Granted ? PermissionStatus.Granted : PermissionStatus.Denied); | |||
} | |||
} | |||
_tcs.TrySetResult(_results); | |||
} | |||
#endregion | |||
#region IPermissionsService Implementation | |||
public Task<PermissionStatus> CheckPermissionStatusAsync(Permission permission) | |||
{ | |||
var names = GetManifestNames(permission); | |||
if (names == null) | |||
{ | |||
Debug.WriteLine("No Android specific permissions needed for: " + permission); | |||
return Task.FromResult(PermissionStatus.Granted); | |||
} | |||
if (names.Count == 0) | |||
{ | |||
Debug.WriteLine("No permissions found in manifest for: " + permission); | |||
return Task.FromResult(PermissionStatus.Unknown); | |||
} | |||
var context = MainApplication.CurrentContext ?? Application.Context; | |||
if (context == null) | |||
{ | |||
Debug.WriteLine("Unable to detect current Activity or Application Context."); | |||
return Task.FromResult(PermissionStatus.Unknown); | |||
} | |||
bool targetsMOrHigher = context.ApplicationInfo.TargetSdkVersion >= Android.OS.BuildVersionCodes.M; | |||
foreach (var name in names) | |||
{ | |||
if (targetsMOrHigher) | |||
{ | |||
if (ContextCompat.CheckSelfPermission(context, name) != Android.Content.PM.Permission.Granted) | |||
return Task.FromResult(PermissionStatus.Denied); | |||
} | |||
else | |||
{ | |||
if (PermissionChecker.CheckSelfPermission(context, name) != PermissionChecker.PermissionGranted) | |||
return Task.FromResult(PermissionStatus.Denied); | |||
} | |||
} | |||
return Task.FromResult(PermissionStatus.Granted); | |||
} | |||
public async Task<Dictionary<Permission, PermissionStatus>> RequestPermissionsAsync(params Permission[] permissions) | |||
{ | |||
if (_tcs != null && !_tcs.Task.IsCompleted) | |||
{ | |||
_tcs.SetCanceled(); | |||
_tcs = null; | |||
} | |||
lock (_locker) | |||
{ | |||
_results = new Dictionary<Permission, PermissionStatus>(); | |||
} | |||
var context = MainApplication.CurrentContext; | |||
if (context == null) | |||
{ | |||
Debug.WriteLine("Unable to detect current Activity."); | |||
foreach (var permission in permissions) | |||
{ | |||
lock (_locker) | |||
{ | |||
if (!_results.ContainsKey(permission)) | |||
_results.Add(permission, PermissionStatus.Unknown); | |||
} | |||
} | |||
return _results; | |||
} | |||
var permissionsToRequest = new List<string>(); | |||
foreach (var permission in permissions) | |||
{ | |||
var result = await CheckPermissionStatusAsync(permission).ConfigureAwait(false); | |||
if (result != PermissionStatus.Granted) | |||
{ | |||
var names = GetManifestNames(permission); | |||
if ((names?.Count ?? 0) == 0) | |||
{ | |||
lock (_locker) | |||
{ | |||
if (!_results.ContainsKey(permission)) | |||
_results.Add(permission, PermissionStatus.Unknown); | |||
} | |||
continue; | |||
} | |||
permissionsToRequest.AddRange(names); | |||
} | |||
else | |||
{ | |||
lock (_locker) | |||
{ | |||
if (!_results.ContainsKey(permission)) | |||
_results.Add(permission, PermissionStatus.Granted); | |||
} | |||
} | |||
} | |||
if (permissionsToRequest.Count == 0) | |||
return _results; | |||
_tcs = new TaskCompletionSource<Dictionary<Permission, PermissionStatus>>(); | |||
ActivityCompat.RequestPermissions((Activity)context, permissionsToRequest.ToArray(), _permissionCode); | |||
return await _tcs.Task.ConfigureAwait(false); | |||
} | |||
#endregion | |||
} | |||
} |
@ -0,0 +1,33 @@ | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace eShopOnContainers.Windows.Helpers | |||
{ | |||
internal class Timeout | |||
{ | |||
public const int Infinite = -1; | |||
readonly CancellationTokenSource _canceller = new CancellationTokenSource(); | |||
public Timeout(int timeout, Action timesUp) | |||
{ | |||
if (timeout == Infinite) | |||
return; | |||
if (timeout < 0) | |||
throw new ArgumentOutOfRangeException("timeoutMilliseconds"); | |||
if (timesUp == null) | |||
throw new ArgumentNullException("timesUp"); | |||
Task.Delay(TimeSpan.FromMilliseconds(timeout), _canceller.Token).ContinueWith(t => | |||
{ | |||
if (!t.IsCanceled) | |||
timesUp(); | |||
}); | |||
} | |||
public void Cancel() | |||
{ | |||
_canceller.Cancel(); | |||
} | |||
} | |||
} |
@ -0,0 +1,127 @@ | |||
using eShopOnContainers.Core.Models.Location; | |||
using eShopOnContainers.Core.Services.Location; | |||
using eShopOnContainers.Windows.Services; | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using Windows.Devices.Geolocation; | |||
using Windows.Foundation; | |||
[assembly: Xamarin.Forms.Dependency(typeof(LocationServiceImplementation))] | |||
namespace eShopOnContainers.Windows.Services | |||
{ | |||
public class LocationServiceImplementation : ILocationServiceImplementation | |||
{ | |||
double _desiredAccuracy; | |||
Geolocator _locator = new Geolocator(); | |||
public LocationServiceImplementation() | |||
{ | |||
DesiredAccuracy = 100; | |||
} | |||
#region Internal Implementation | |||
static Position GetPosition(Geoposition position) | |||
{ | |||
var pos = new Position | |||
{ | |||
Accuracy = position.Coordinate.Accuracy, | |||
Altitude = position.Coordinate.Point.Position.Altitude, | |||
Latitude = position.Coordinate.Point.Position.Latitude, | |||
Longitude = position.Coordinate.Point.Position.Longitude, | |||
Timestamp = position.Coordinate.Timestamp.ToUniversalTime() | |||
}; | |||
if (position.Coordinate.Heading != null) | |||
pos.Heading = position.Coordinate.Heading.Value; | |||
if (position.Coordinate.Speed != null) | |||
pos.Speed = position.Coordinate.Speed.Value; | |||
if (position.Coordinate.AltitudeAccuracy.HasValue) | |||
pos.AltitudeAccuracy = position.Coordinate.AltitudeAccuracy.Value; | |||
return pos; | |||
} | |||
#endregion | |||
#region ILocationServiceImplementation | |||
public double DesiredAccuracy | |||
{ | |||
get { return _desiredAccuracy; } | |||
set | |||
{ | |||
_desiredAccuracy = value; | |||
_locator.DesiredAccuracy = (value < 100) ? PositionAccuracy.High : PositionAccuracy.Default; | |||
} | |||
} | |||
public bool IsGeolocationAvailable | |||
{ | |||
get | |||
{ | |||
var status = _locator.LocationStatus; | |||
while (status == PositionStatus.Initializing) | |||
{ | |||
Task.Delay(10).Wait(); | |||
status = _locator.LocationStatus; | |||
} | |||
return status != PositionStatus.NotAvailable; | |||
} | |||
} | |||
public bool IsGeolocationEnabled | |||
{ | |||
get | |||
{ | |||
var status = _locator.LocationStatus; | |||
while (status == PositionStatus.Initializing) | |||
{ | |||
Task.Delay(10).Wait(); | |||
status = _locator.LocationStatus; | |||
} | |||
return status != PositionStatus.Disabled && status != PositionStatus.NotAvailable; | |||
} | |||
} | |||
public Task<Position> GetPositionAsync(TimeSpan? timeout = null, CancellationToken? cancelToken = null) | |||
{ | |||
var timeoutMilliseconds = timeout.HasValue ? (int)timeout.Value.TotalMilliseconds : eShopOnContainers.Windows.Helpers.Timeout.Infinite; | |||
if (timeoutMilliseconds < 0 && timeoutMilliseconds != eShopOnContainers.Windows.Helpers.Timeout.Infinite) | |||
throw new ArgumentOutOfRangeException(nameof(timeout)); | |||
if (!cancelToken.HasValue) | |||
cancelToken = CancellationToken.None; | |||
var pos = _locator.GetGeopositionAsync(TimeSpan.FromTicks(0), TimeSpan.FromDays(365)); | |||
cancelToken.Value.Register(o => ((IAsyncOperation<Geoposition>)o).Cancel(), pos); | |||
var timer = new eShopOnContainers.Windows.Helpers.Timeout(timeoutMilliseconds, pos.Cancel); | |||
var tcs = new TaskCompletionSource<Position>(); | |||
pos.Completed = (op, s) => | |||
{ | |||
timer.Cancel(); | |||
switch (s) | |||
{ | |||
case AsyncStatus.Canceled: | |||
tcs.SetCanceled(); | |||
break; | |||
case AsyncStatus.Completed: | |||
tcs.SetResult(GetPosition(op.GetResults())); | |||
break; | |||
case AsyncStatus.Error: | |||
var ex = op.ErrorCode; | |||
if (ex is UnauthorizedAccessException) | |||
ex = new GeolocationException(GeolocationError.Unauthorized, ex); | |||
tcs.SetException(ex); | |||
break; | |||
} | |||
}; | |||
return tcs.Task; | |||
} | |||
#endregion | |||
} | |||
} |
@ -0,0 +1,110 @@ | |||
using CoreLocation; | |||
using eShopOnContainers.Core.Models.Location; | |||
using Foundation; | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace eShopOnContainers.iOS.Services | |||
{ | |||
internal class GeolocationSingleUpdateDelegate : CLLocationManagerDelegate | |||
{ | |||
bool _haveLocation; | |||
readonly Position _position = new Position(); | |||
readonly double _desiredAccuracy; | |||
readonly TaskCompletionSource<Position> _tcs; | |||
readonly CLLocationManager _manager; | |||
public Task<Position> Task => _tcs?.Task; | |||
public GeolocationSingleUpdateDelegate(CLLocationManager manager, double desiredAccuracy, int timeout, CancellationToken cancelToken) | |||
{ | |||
_manager = manager; | |||
_tcs = new TaskCompletionSource<Position>(manager); | |||
_desiredAccuracy = desiredAccuracy; | |||
if (timeout != Timeout.Infinite) | |||
{ | |||
Timer t = null; | |||
t = new Timer(s => | |||
{ | |||
if (_haveLocation) | |||
_tcs.TrySetResult(new Position(_position)); | |||
else | |||
_tcs.TrySetCanceled(); | |||
StopListening(); | |||
t.Dispose(); | |||
}, null, timeout, 0); | |||
} | |||
cancelToken.Register(() => | |||
{ | |||
StopListening(); | |||
_tcs.TrySetCanceled(); | |||
}); | |||
} | |||
public override void AuthorizationChanged(CLLocationManager manager, CLAuthorizationStatus status) | |||
{ | |||
// If user has services disabled, throw an exception for consistency. | |||
if (status == CLAuthorizationStatus.Denied || status == CLAuthorizationStatus.Restricted) | |||
{ | |||
StopListening(); | |||
_tcs.TrySetException(new GeolocationException(GeolocationError.Unauthorized)); | |||
} | |||
} | |||
public override void Failed(CLLocationManager manager, NSError error) | |||
{ | |||
switch ((CLError)(int)error.Code) | |||
{ | |||
case CLError.Network: | |||
StopListening(); | |||
_tcs.SetException(new GeolocationException(GeolocationError.PositionUnavailable)); | |||
break; | |||
case CLError.LocationUnknown: | |||
StopListening(); | |||
_tcs.TrySetException(new GeolocationException(GeolocationError.PositionUnavailable)); | |||
break; | |||
} | |||
} | |||
public override void UpdatedLocation(CLLocationManager manager, CLLocation newLocation, CLLocation oldLocation) | |||
{ | |||
if (newLocation.HorizontalAccuracy < 0) | |||
return; | |||
if (_haveLocation && newLocation.HorizontalAccuracy > _position.Accuracy) | |||
return; | |||
_position.Accuracy = newLocation.HorizontalAccuracy; | |||
_position.Altitude = newLocation.Altitude; | |||
_position.AltitudeAccuracy = newLocation.VerticalAccuracy; | |||
_position.Latitude = newLocation.Coordinate.Latitude; | |||
_position.Longitude = newLocation.Coordinate.Longitude; | |||
_position.Speed = newLocation.Speed; | |||
try | |||
{ | |||
_position.Timestamp = new DateTimeOffset((DateTime)newLocation.Timestamp); | |||
} | |||
catch (Exception) | |||
{ | |||
_position.Timestamp = DateTimeOffset.UtcNow; | |||
} | |||
_haveLocation = true; | |||
if (_position.Accuracy <= _desiredAccuracy) | |||
{ | |||
_tcs.TrySetResult(new Position(_position)); | |||
StopListening(); | |||
} | |||
} | |||
private void StopListening() | |||
{ | |||
_manager.StopUpdatingLocation(); | |||
} | |||
} | |||
} |
@ -0,0 +1,99 @@ | |||
using CoreLocation; | |||
using eShopOnContainers.Core.Models.Location; | |||
using eShopOnContainers.Core.Models.Permissions; | |||
using eShopOnContainers.Core.Services.Location; | |||
using eShopOnContainers.Core.Services.Permissions; | |||
using eShopOnContainers.iOS.Services; | |||
using Foundation; | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using UIKit; | |||
[assembly: Xamarin.Forms.Dependency(typeof(LocationServiceImplementation))] | |||
namespace eShopOnContainers.iOS.Services | |||
{ | |||
public class LocationServiceImplementation : ILocationServiceImplementation | |||
{ | |||
Lazy<IPermissionsService> _permissionsService; | |||
public LocationServiceImplementation() | |||
{ | |||
DesiredAccuracy = 100; | |||
_permissionsService = new Lazy<IPermissionsService>(() => new PermissionsService(), LazyThreadSafetyMode.PublicationOnly); | |||
} | |||
#region Internal Implementation | |||
async Task<bool> CheckPermissions(Permission permission) | |||
{ | |||
var status = await _permissionsService.Value.CheckPermissionStatusAsync(permission); | |||
if (status != PermissionStatus.Granted) | |||
{ | |||
Console.WriteLine("Currently do not have Location permissions, requesting permissions"); | |||
var request = await _permissionsService.Value.RequestPermissionsAsync(permission); | |||
if (request[permission] != PermissionStatus.Granted) | |||
{ | |||
Console.WriteLine("Location permission denied, can not get positions async."); | |||
return false; | |||
} | |||
} | |||
return true; | |||
} | |||
CLLocationManager GetManager() | |||
{ | |||
CLLocationManager manager = null; | |||
new NSObject().InvokeOnMainThread(() => manager = new CLLocationManager()); | |||
return manager; | |||
} | |||
#endregion | |||
#region ILocationServiceImplementation | |||
public double DesiredAccuracy { get; set; } | |||
public bool IsGeolocationAvailable => true; | |||
public bool IsGeolocationEnabled | |||
{ | |||
get | |||
{ | |||
var status = CLLocationManager.Status; | |||
return CLLocationManager.LocationServicesEnabled; | |||
} | |||
} | |||
public async Task<Position> GetPositionAsync(TimeSpan? timeout, CancellationToken? cancelToken = null) | |||
{ | |||
var permission = Permission.LocationWhenInUse; | |||
var hasPermission = await CheckPermissions(permission); | |||
if (!hasPermission) | |||
throw new GeolocationException(GeolocationError.Unauthorized); | |||
var timeoutMilliseconds = timeout.HasValue ? (int)timeout.Value.TotalMilliseconds : Timeout.Infinite; | |||
if (timeoutMilliseconds <= 0 && timeoutMilliseconds != Timeout.Infinite) | |||
throw new ArgumentOutOfRangeException(nameof(timeout), "Timeout must be positive or Timeout.Infinite"); | |||
if (!cancelToken.HasValue) | |||
cancelToken = CancellationToken.None; | |||
TaskCompletionSource<Position> tcs; | |||
var manager = GetManager(); | |||
manager.DesiredAccuracy = DesiredAccuracy; | |||
// Always prevent location update pausing since we're only listening for a single update | |||
if (UIDevice.CurrentDevice.CheckSystemVersion(6, 0)) | |||
manager.PausesLocationUpdatesAutomatically = false; | |||
tcs = new TaskCompletionSource<Position>(manager); | |||
var singleListener = new GeolocationSingleUpdateDelegate(manager, DesiredAccuracy, timeoutMilliseconds, cancelToken.Value); | |||
manager.Delegate = singleListener; | |||
manager.StartUpdatingLocation(); | |||
return await singleListener.Task; | |||
} | |||
#endregion | |||
} | |||
} |
@ -0,0 +1,128 @@ | |||
using CoreLocation; | |||
using eShopOnContainers.Core.Models.Permissions; | |||
using eShopOnContainers.Core.Services.Permissions; | |||
using Foundation; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Threading.Tasks; | |||
using UIKit; | |||
namespace eShopOnContainers.iOS.Services | |||
{ | |||
public class PermissionsService : IPermissionsService | |||
{ | |||
CLLocationManager _locationManager; | |||
#region Internal Implementation | |||
PermissionStatus GetLocationPermissionStatus(Permission permission) | |||
{ | |||
if (!CLLocationManager.LocationServicesEnabled) | |||
return PermissionStatus.Disabled; | |||
var status = CLLocationManager.Status; | |||
if (UIDevice.CurrentDevice.CheckSystemVersion(8, 0)) | |||
{ | |||
switch (status) | |||
{ | |||
case CLAuthorizationStatus.AuthorizedAlways: | |||
case CLAuthorizationStatus.AuthorizedWhenInUse: | |||
return PermissionStatus.Granted; | |||
case CLAuthorizationStatus.Denied: | |||
return PermissionStatus.Denied; | |||
case CLAuthorizationStatus.Restricted: | |||
return PermissionStatus.Restricted; | |||
default: | |||
return PermissionStatus.Unknown; | |||
} | |||
} | |||
switch (status) | |||
{ | |||
case CLAuthorizationStatus.Authorized: | |||
return PermissionStatus.Granted; | |||
case CLAuthorizationStatus.Denied: | |||
return PermissionStatus.Denied; | |||
case CLAuthorizationStatus.Restricted: | |||
return PermissionStatus.Restricted; | |||
default: | |||
return PermissionStatus.Unknown; | |||
} | |||
} | |||
Task<PermissionStatus> RequestLocationPermissionAsync(Permission permission = Permission.Location) | |||
{ | |||
if (CLLocationManager.Status == CLAuthorizationStatus.AuthorizedWhenInUse && permission == Permission.LocationAlways) | |||
{ | |||
// Don't do anything and request it | |||
} | |||
else if (GetLocationPermissionStatus(permission) != PermissionStatus.Unknown) | |||
return Task.FromResult(GetLocationPermissionStatus(permission)); | |||
if (!UIDevice.CurrentDevice.CheckSystemVersion(8, 0)) | |||
{ | |||
return Task.FromResult(PermissionStatus.Unknown); | |||
} | |||
EventHandler<CLAuthorizationChangedEventArgs> authCallback = null; | |||
var tcs = new TaskCompletionSource<PermissionStatus>(); | |||
_locationManager = new CLLocationManager(); | |||
authCallback = (sender, e) => | |||
{ | |||
if (e.Status == CLAuthorizationStatus.NotDetermined) | |||
return; | |||
_locationManager.AuthorizationChanged -= authCallback; | |||
tcs.TrySetResult(GetLocationPermissionStatus(permission)); | |||
}; | |||
_locationManager.AuthorizationChanged += authCallback; | |||
var info = NSBundle.MainBundle.InfoDictionary; | |||
if (permission == Permission.LocationWhenInUse) | |||
{ | |||
if (info.ContainsKey(new NSString("NSLocationWhenInUseUsageDescription"))) | |||
_locationManager.RequestWhenInUseAuthorization(); | |||
else | |||
throw new UnauthorizedAccessException("On iOS 8.0 and higher you must set either NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription in your Info.plist file to enable Authorization Requests for Location updates."); | |||
} | |||
return tcs.Task; | |||
} | |||
#endregion | |||
#region IPermissionsServiceImplementation | |||
public Task<PermissionStatus> CheckPermissionStatusAsync(Permission permission) | |||
{ | |||
switch (permission) | |||
{ | |||
case Permission.LocationWhenInUse: | |||
return Task.FromResult(GetLocationPermissionStatus(permission)); | |||
} | |||
return Task.FromResult(PermissionStatus.Granted); | |||
} | |||
public async Task<Dictionary<Permission, PermissionStatus>> RequestPermissionsAsync(params Permission[] permissions) | |||
{ | |||
var results = new Dictionary<Permission, PermissionStatus>(); | |||
foreach (var permission in permissions) | |||
{ | |||
if (results.ContainsKey(permission)) | |||
continue; | |||
switch (permission) | |||
{ | |||
case Permission.LocationWhenInUse: | |||
results.Add(permission, await RequestLocationPermissionAsync(permission).ConfigureAwait(false)); | |||
break; | |||
} | |||
if (!results.ContainsKey(permission)) | |||
results.Add(permission, PermissionStatus.Granted); | |||
} | |||
return results; | |||
} | |||
#endregion | |||
} | |||
} |