From 04296e7a14120dc42b718fffa9c61217d2726d04 Mon Sep 17 00:00:00 2001 From: David Britch Date: Tue, 20 Feb 2018 15:28:06 +0000 Subject: [PATCH] Android LocationService implementation complete. --- .../eShopOnContainers.Core/App.xaml.cs | 2 - .../ILocationServiceImplementation.cs | 3 - .../Activities/MainActivity.cs | 5 +- .../Extensions/LocationExtensions.cs | 85 ++++++++++ .../Services/GeolocationSingleListener.cs | 112 +++++++++++++ .../Services/LocationServiceImplementation.cs | 155 ++++++++++++++++++ ...issionService.cs => PermissionsService.cs} | 7 +- .../eShopOnContainers.Droid.csproj | 5 +- 8 files changed, 363 insertions(+), 11 deletions(-) create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Extensions/LocationExtensions.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/GeolocationSingleListener.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/LocationServiceImplementation.cs rename src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/{PermissionService.cs => PermissionsService.cs} (98%) diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs index 7207e2204..2d7322d14 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs @@ -77,8 +77,6 @@ namespace eShopOnContainers //locationService.AllowsBackgroundUpdates = true; locator.DesiredAccuracy = 50; - await Task.Delay(5000); - var position = await locator.GetPositionAsync(); _settingsService.Latitude = position.Latitude.ToString(); diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationServiceImplementation.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationServiceImplementation.cs index c83a710ea..ba85764ea 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationServiceImplementation.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationServiceImplementation.cs @@ -7,9 +7,6 @@ namespace eShopOnContainers.Core.Services.Location { public interface ILocationServiceImplementation { - event EventHandler PositionError; - event EventHandler PositionChanged; - double DesiredAccuracy { get; set; } bool IsGeolocationAvailable { get; } bool IsGeolocationEnabled { get; } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Activities/MainActivity.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Activities/MainActivity.cs index 12ea046ac..aef0ef1b3 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Activities/MainActivity.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Activities/MainActivity.cs @@ -7,9 +7,9 @@ using Android.Runtime; using Android.Views; using FFImageLoading; using FFImageLoading.Forms.Droid; -using Plugin.Permissions; using System; using Xamarin.Forms.Platform.Android; +using eShopOnContainers.Droid.Services; namespace eShopOnContainers.Droid.Activities { @@ -57,8 +57,7 @@ namespace eShopOnContainers.Droid.Activities public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) { base.OnRequestPermissionsResult(requestCode, permissions, grantResults); - - PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults); + PermissionsService.Instance.OnRequestPermissionResult(requestCode, permissions, grantResults); } } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Extensions/LocationExtensions.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Extensions/LocationExtensions.cs new file mode 100644 index 000000000..abb76c1f5 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Extensions/LocationExtensions.cs @@ -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); + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/GeolocationSingleListener.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/GeolocationSingleListener.cs new file mode 100644 index 000000000..db8f13198 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/GeolocationSingleListener.cs @@ -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 _tcs = new TaskCompletionSource(); + HashSet _activeProviders = new HashSet(); + Android.Locations.Location _bestLocation; + + public Task Task => _tcs.Task; + + public GeolocationSingleListener(LocationManager manager, float desiredAccuracy, int timeout, IEnumerable activeProviders, Action finishedCallback) + { + _desiredAccuracy = desiredAccuracy; + _finishedCallback = finishedCallback; + + _activeProviders = new HashSet(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()); + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/LocationServiceImplementation.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/LocationServiceImplementation.cs new file mode 100644 index 000000000..f17d1bef6 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/LocationServiceImplementation.cs @@ -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 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 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(); + + var providers = new List(); + 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 + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionsService.cs similarity index 98% rename from src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionService.cs rename to src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionsService.cs index 268d71771..36399cdbe 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionsService.cs @@ -9,11 +9,10 @@ using Android.App; using Android.Support.V4.App; using Android.Support.V4.Content; using System.Diagnostics; -using Android.Icu.Text; namespace eShopOnContainers.Droid.Services { - public class PermissionService : IPermissionsService + public class PermissionsService : IPermissionsService { const int _permissionCode = 25; object _locker = new object(); @@ -21,6 +20,8 @@ namespace eShopOnContainers.Droid.Services Dictionary _results; IList _requestedPermissions; + internal static PermissionsService Instance; + #region Internal Implementation List GetManifestNames(Permission permission) @@ -134,6 +135,8 @@ namespace eShopOnContainers.Droid.Services public Task CheckPermissionStatusAsync(Permission permission) { + Instance = this; + var names = GetManifestNames(permission); if (names == null) { diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj index 83189bc72..7d699f9d2 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj @@ -212,7 +212,10 @@ - + + + +