From 41398ab03ab2b3202415e62e49ecde9a1c1c9d99 Mon Sep 17 00:00:00 2001 From: David Britch Date: Tue, 20 Feb 2018 12:22:58 +0000 Subject: [PATCH] Work in progress. --- .../eShopOnContainers.Core/App.xaml.cs | 9 +- .../Models/Location/ActivityType.cs | 10 + .../Models/Location/Address.cs | 38 +++ .../Models/Location/GeolocationError.cs | 8 + .../Models/Location/GeolocationException.cs | 27 ++ .../Models/Location/ListenerSettings.cs | 15 ++ .../Models/Location/Position.cs | 43 ++++ .../Models/Location/PositionErrorEventArgs.cs | 14 + .../Models/Location/PositionEventArgs.cs | 17 ++ .../Models/Permissions/Permission.cs | 10 + .../Models/Permissions/PermissionStatus.cs | 11 + .../ILocationServiceImplementation.cs | 19 ++ .../Services/Location/LocationService.cs | 3 - .../Permissions/IPermissionsService.cs | 12 + .../ViewModels/SettingsViewModel.cs | 8 +- .../eShopOnContainers.Core.csproj | 5 +- .../Activities/MainActivity.cs | 1 + .../MainApplication.cs | 10 +- .../Services/PermissionService.cs | 241 ++++++++++++++++++ .../eShopOnContainers.Droid.csproj | 16 +- .../eShopOnContainers.Droid/packages.config | 3 - .../GeolocationSingleUpdateDelegate.cs | 139 ++++++++++ .../Services/LocationServiceImplementation.cs | 150 +++++++++++ .../Services/PermissionsService.cs | 143 +++++++++++ .../eShopOnContainers.iOS.csproj | 9 +- .../eShopOnContainers.iOS/packages.config | 1 - 26 files changed, 923 insertions(+), 39 deletions(-) create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/ActivityType.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/Address.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/GeolocationError.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/GeolocationException.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/ListenerSettings.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/Position.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/PositionErrorEventArgs.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/PositionEventArgs.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Permissions/Permission.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Permissions/PermissionStatus.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationServiceImplementation.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Permissions/IPermissionsService.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionService.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/GeolocationSingleUpdateDelegate.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/LocationServiceImplementation.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/PermissionsService.cs diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs index 0270bb961..7207e2204 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs @@ -3,11 +3,11 @@ using eShopOnContainers.Core.Services.Location; using eShopOnContainers.Core.Services.Settings; using eShopOnContainers.Core.ViewModels.Base; using eShopOnContainers.Services; -using Plugin.Geolocator; using System.Globalization; using System.Threading.Tasks; using Xamarin.Forms; using Xamarin.Forms.Xaml; +using eShopOnContainers.Core.Services.Dependency; [assembly: XamlCompilation(XamlCompilationOptions.Compile)] namespace eShopOnContainers @@ -69,13 +69,16 @@ namespace eShopOnContainers private async Task GetGpsLocation() { - var locator = CrossGeolocator.Current; + var dependencyService = ViewModelLocator.Resolve(); + var locator = dependencyService.Get(); if (locator.IsGeolocationEnabled && locator.IsGeolocationAvailable) { - locator.AllowsBackgroundUpdates = true; + //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/Models/Location/ActivityType.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/ActivityType.cs new file mode 100644 index 000000000..3240b1bad --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/ActivityType.cs @@ -0,0 +1,10 @@ +namespace eShopOnContainers.Core.Models.Location +{ + public enum ActivityType + { + Other, + AutomotiveNavigation, + Fitness, + OtherNavigation + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/Address.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/Address.cs new file mode 100644 index 000000000..a4be9e1c7 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/Address.cs @@ -0,0 +1,38 @@ +using System; + +namespace eShopOnContainers.Core.Models.Location +{ + public class Address + { + public double Latitude { get; set; } + public double Longitude { get; set; } + public string CountryCode { get; set; } + public string CountryName { get; set; } + public string FeatureName { get; set; } + public string PostalCode { get; set; } + public string SubLocality { get; set; } + public string Thoroughfare { get; set; } + public string SubThoroughfare { get; set; } + public string Locality { get; set; } + public string AdminArea { get; set; } + public string SubAdminArea { get; set; } + + public Address(Address address) + { + if (address == null) + throw new ArgumentNullException(nameof(address)); + + CountryCode = address.CountryCode; + CountryName = address.CountryName; + Latitude = address.Latitude; + Longitude = address.Longitude; + FeatureName = address.FeatureName; + PostalCode = address.PostalCode; + SubLocality = address.SubLocality; + Thoroughfare = address.Thoroughfare; + SubThoroughfare = address.SubThoroughfare; + SubAdminArea = address.SubAdminArea; + AdminArea = address.AdminArea; + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/GeolocationError.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/GeolocationError.cs new file mode 100644 index 000000000..d8ac11184 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/GeolocationError.cs @@ -0,0 +1,8 @@ +namespace eShopOnContainers.Core.Models.Location +{ + public enum GeolocationError + { + PositionUnavailable, + Unauthorized + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/GeolocationException.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/GeolocationException.cs new file mode 100644 index 000000000..39b89483a --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/GeolocationException.cs @@ -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; + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/ListenerSettings.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/ListenerSettings.cs new file mode 100644 index 000000000..40849b86f --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/ListenerSettings.cs @@ -0,0 +1,15 @@ +using System; + +namespace eShopOnContainers.Core.Models.Location +{ + public class ListenerSettings + { + public bool AllowBackgroundUpdates { get; set; } = false; + public bool PauseLocationUpdatesAutomatically { get; set; } = true; + public ActivityType ActivityType { get; set; } = ActivityType.Other; + public bool ListenForSignificantChanges { get; set; } = false; + public bool DeferLocationUpdates { get; set; } = false; + public double? DeferralDistanceMeters { get; set; } = 500; + public TimeSpan? DeferralTime { get; set; } = TimeSpan.FromMinutes(5); + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/Position.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/Position.cs new file mode 100644 index 000000000..fab7ed732 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/Position.cs @@ -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; + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/PositionErrorEventArgs.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/PositionErrorEventArgs.cs new file mode 100644 index 000000000..6c5e3cd24 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/PositionErrorEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace eShopOnContainers.Core.Models.Location +{ + public class PositionErrorEventArgs : EventArgs + { + public GeolocationError Error { get; private set; } + + public PositionErrorEventArgs(GeolocationError error) + { + Error = error; + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/PositionEventArgs.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/PositionEventArgs.cs new file mode 100644 index 000000000..2107b05e5 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/PositionEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace eShopOnContainers.Core.Models.Location +{ + public class PositionEventArgs : EventArgs + { + public Position Position { get; private set; } + + public PositionEventArgs(Position position) + { + if (position == null) + throw new ArgumentNullException("position"); + + Position = position; + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Permissions/Permission.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Permissions/Permission.cs new file mode 100644 index 000000000..bee5de424 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Permissions/Permission.cs @@ -0,0 +1,10 @@ +namespace eShopOnContainers.Core.Models.Permissions +{ + public enum Permission + { + Unknown, + Location, + LocationAlways, + LocationWhenInUse + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Permissions/PermissionStatus.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Permissions/PermissionStatus.cs new file mode 100644 index 000000000..2a8f789f6 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Permissions/PermissionStatus.cs @@ -0,0 +1,11 @@ +namespace eShopOnContainers.Core.Models.Permissions +{ + public enum PermissionStatus + { + Denied, + Disabled, + Granted, + Restricted, + Unknown + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationServiceImplementation.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationServiceImplementation.cs new file mode 100644 index 000000000..c83a710ea --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationServiceImplementation.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using eShopOnContainers.Core.Models.Location; + +namespace eShopOnContainers.Core.Services.Location +{ + public interface ILocationServiceImplementation + { + event EventHandler PositionError; + event EventHandler PositionChanged; + + double DesiredAccuracy { get; set; } + bool IsGeolocationAvailable { get; } + bool IsGeolocationEnabled { get; } + + Task GetPositionAsync(TimeSpan? timeout = null, CancellationToken? token = null, bool includeHeading = false); + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/LocationService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/LocationService.cs index deaa60936..f20ae6339 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/LocationService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/LocationService.cs @@ -16,11 +16,8 @@ namespace eShopOnContainers.Core.Services.Location public async Task UpdateUserLocation(eShopOnContainers.Core.Models.Location.Location newLocReq, string token) { UriBuilder builder = new UriBuilder(GlobalSetting.Instance.LocationEndpoint); - builder.Path = "api/v1/locations"; - string uri = builder.ToString(); - await _requestProvider.PostAsync(uri, newLocReq, token); } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Permissions/IPermissionsService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Permissions/IPermissionsService.cs new file mode 100644 index 000000000..b72061cc1 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Permissions/IPermissionsService.cs @@ -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 CheckPermissionStatusAsync(Permission permission); + Task> RequestPermissionsAsync(params Permission[] permissions); + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs index 1e478b85c..8e86847b7 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs @@ -3,11 +3,11 @@ using eShopOnContainers.Core.Models.User; using eShopOnContainers.Core.Services.Location; using eShopOnContainers.Core.Services.Settings; using eShopOnContainers.Core.ViewModels.Base; -using Plugin.Geolocator; using System.Globalization; using System.Threading.Tasks; using System.Windows.Input; using Xamarin.Forms; +using eShopOnContainers.Core.Services.Dependency; namespace eShopOnContainers.Core.ViewModels { @@ -29,11 +29,13 @@ namespace eShopOnContainers.Core.ViewModels private readonly ISettingsService _settingsService; private readonly ILocationService _locationService; + private readonly IDependencyService _dependencyService; - public SettingsViewModel(ISettingsService settingsService, ILocationService locationService) + public SettingsViewModel(ISettingsService settingsService, ILocationService locationService, IDependencyService dependencyService) { _settingsService = settingsService; _locationService = locationService; + _dependencyService = dependencyService; _useAzureServices = !_settingsService.UseMocks; _endpoint = _settingsService.UrlBase; @@ -342,7 +344,7 @@ namespace eShopOnContainers.Core.ViewModels { if (_allowGpsLocation) { - var locator = CrossGeolocator.Current; + var locator = _dependencyService.Get(); if (!locator.IsGeolocationEnabled) { _allowGpsLocation = false; diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj index 06de19f11..26fb0d89a 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj @@ -10,9 +10,12 @@ - + + + + diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Activities/MainActivity.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Activities/MainActivity.cs index d72898f40..12ea046ac 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Activities/MainActivity.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Activities/MainActivity.cs @@ -57,6 +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); } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/MainApplication.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/MainApplication.cs index 74e5a3883..8096eb268 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/MainApplication.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/MainApplication.cs @@ -1,7 +1,7 @@ using Android.App; +using Android.Content; using Android.OS; using Android.Runtime; -using Plugin.CurrentActivity; using System; namespace eShopOnContainers.Droid @@ -9,6 +9,8 @@ namespace eShopOnContainers.Droid [Application] public class MainApplication : Application, Application.IActivityLifecycleCallbacks { + internal static Context CurrentContext { get; private set; } + public MainApplication(IntPtr handle, JniHandleOwnership transer) : base(handle, transer) { @@ -28,7 +30,7 @@ namespace eShopOnContainers.Droid public void OnActivityCreated(Activity activity, Bundle savedInstanceState) { - CrossCurrentActivity.Current.Activity = activity; + CurrentContext = activity; } public void OnActivityDestroyed(Activity activity) @@ -41,7 +43,7 @@ namespace eShopOnContainers.Droid public void OnActivityResumed(Activity activity) { - CrossCurrentActivity.Current.Activity = activity; + CurrentContext = activity; } public void OnActivitySaveInstanceState(Activity activity, Bundle outState) @@ -50,7 +52,7 @@ namespace eShopOnContainers.Droid public void OnActivityStarted(Activity activity) { - CrossCurrentActivity.Current.Activity = activity; + CurrentContext = activity; } public void OnActivityStopped(Activity activity) diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionService.cs new file mode 100644 index 000000000..268d71771 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/Services/PermissionService.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using eShopOnContainers.Core.Models.Permissions; +using eShopOnContainers.Core.Services.Permissions; +using System.Linq; +using Android; +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 + { + const int _permissionCode = 25; + object _locker = new object(); + TaskCompletionSource> _tcs; + Dictionary _results; + IList _requestedPermissions; + + #region Internal Implementation + + List GetManifestNames(Permission permission) + { + var permissionNames = new List(); + 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; + } + + static 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 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> RequestPermissionsAsync(params Permission[] permissions) + { + if (_tcs != null && !_tcs.Task.IsCompleted) + { + _tcs.SetCanceled(); + _tcs = null; + } + lock (_locker) + { + _results = new Dictionary(); + } + + 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(); + 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>(); + ActivityCompat.RequestPermissions((Activity)context, permissionsToRequest.ToArray(), _permissionCode); + return await _tcs.Task.ConfigureAwait(false); + } + + #endregion + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj index 18c325634..83189bc72 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj @@ -145,27 +145,12 @@ ..\..\..\..\packages\Acr.UserDialogs.6.5.1\lib\MonoAndroid10\Acr.UserDialogs.Interface.dll - - ..\..\..\..\packages\Plugin.CurrentActivity.1.0.1\lib\MonoAndroid10\Plugin.CurrentActivity.dll - - - ..\..\..\..\packages\Plugin.Permissions.2.2.1\lib\MonoAndroid10\Plugin.Permissions.Abstractions.dll - - - ..\..\..\..\packages\Plugin.Permissions.2.2.1\lib\MonoAndroid10\Plugin.Permissions.dll - ..\..\..\..\packages\SlideOverKit.2.1.5\lib\MonoAndroid10\SlideOverKit.dll ..\..\..\..\packages\SlideOverKit.2.1.5\lib\MonoAndroid10\SlideOverKit.Droid.dll - - ..\..\..\..\packages\Xam.Plugin.Geolocator.3.0.4\lib\MonoAndroid10\Plugin.Geolocator.Abstractions.dll - - - ..\..\..\..\packages\Xam.Plugin.Geolocator.3.0.4\lib\MonoAndroid10\Plugin.Geolocator.dll - ..\..\..\..\packages\Microsoft.Net.Http.2.2.28\lib\monoandroid\System.Net.Http.Extensions.dll @@ -227,6 +212,7 @@ + diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/packages.config b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/packages.config index 274cc15e9..2efab0dea 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/packages.config +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/packages.config @@ -17,8 +17,6 @@ - - @@ -69,7 +67,6 @@ - diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/GeolocationSingleUpdateDelegate.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/GeolocationSingleUpdateDelegate.cs new file mode 100644 index 000000000..110ac1eff --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/GeolocationSingleUpdateDelegate.cs @@ -0,0 +1,139 @@ +using System; +using CoreLocation; +using Foundation; +using System.Threading.Tasks; +using System.Threading; +using System.Linq; +using eShopOnContainers.Core.Models.Location; + +namespace eShopOnContainers.iOS.Services +{ + internal class GeolocationSingleUpdateDelegate : CLLocationManagerDelegate + { + bool _haveHeading; + bool _haveLocation; + readonly Position _position = new Position(); + CLHeading _bestHeading; + + readonly double _desiredAccuracy; + readonly bool _includeHeading; + readonly TaskCompletionSource _tcs; + readonly CLLocationManager _manager; + + public Task Task => _tcs?.Task; + + public GeolocationSingleUpdateDelegate(CLLocationManager manager, double desiredAccuracy, bool includeHeading, int timeout, CancellationToken cancelToken) + { + _manager = manager; + _tcs = new TaskCompletionSource(manager); + _desiredAccuracy = desiredAccuracy; + _includeHeading = includeHeading; + + 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 bool ShouldDisplayHeadingCalibration(CLLocationManager manager) => true; + + 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 ex) + { + _position.Timestamp = DateTimeOffset.UtcNow; + } + _haveLocation = true; + + if ((!_includeHeading || _haveHeading) && _position.Accuracy <= _desiredAccuracy) + { + _tcs.TrySetResult(new Position(_position)); + StopListening(); + } + } + + public override void UpdatedHeading(CLLocationManager manager, CLHeading newHeading) + { + if (newHeading.HeadingAccuracy < 0) + return; + if (_bestHeading != null && newHeading.HeadingAccuracy >= _bestHeading.HeadingAccuracy) + return; + + _bestHeading = newHeading; + _position.Heading = newHeading.TrueHeading; + _haveHeading = true; + + if (_haveLocation && _position.Accuracy <= _desiredAccuracy) + { + _tcs.TrySetResult(new Position(_position)); + StopListening(); + } + } + + private void StopListening() + { + if (CLLocationManager.HeadingAvailable) + _manager.StopUpdatingHeading(); + + _manager.StopUpdatingLocation(); + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/LocationServiceImplementation.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/LocationServiceImplementation.cs new file mode 100644 index 000000000..12bef0775 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/LocationServiceImplementation.cs @@ -0,0 +1,150 @@ +using eShopOnContainers.iOS.Services; +using eShopOnContainers.Core.Services.Location; +using CoreLocation; +using eShopOnContainers.Core.Models.Location; +using Foundation; +using System; +using System.Threading.Tasks; +using System.Threading; +using UIKit; +using eShopOnContainers.Core.Models.Permissions; +using eShopOnContainers.Core.Services.Permissions; + +[assembly: Xamarin.Forms.Dependency(typeof(LocationServiceImplementation))] +namespace eShopOnContainers.iOS.Services +{ + public class LocationServiceImplementation : ILocationServiceImplementation + { + bool _deferringUpdates; + readonly CLLocationManager _manager; + Position _lastPosition; + + public event EventHandler PositionError; + public event EventHandler PositionChanged; + public double DesiredAccuracy { get; set; } + public bool IsGeolocationAvailable => true; + public bool IsGeolocationEnabled + { + get + { + var status = CLLocationManager.Status; + return CLLocationManager.LocationServicesEnabled; + } + } + + public bool SupportsHeading => CLLocationManager.HeadingAvailable; + + public LocationServiceImplementation() + { + DesiredAccuracy = 100; + //_manager = GetManager(); + //_manager.AuthorizationChanged += OnAuthorizationChanged; + //_manager.Failed += OnFailed; + //_manager.UpdatedLocation += OnUpdatedLocation; + //_manager.UpdatedHeading += OnUpdatedHeading; + //_manager.DeferredUpdatesFinished += OnDeferredUpdatesFinished; + } + + void OnDeferredUpdatesFinished(object sender, NSErrorEventArgs e) => _deferringUpdates = false; + + #region Internal Implementation + + async Task CheckPermissions(Permission permission) + { + IPermissionsService permissionsService = new PermissionsService(); + var status = await permissionsService.CheckPermissionStatusAsync(permission); + if (status != PermissionStatus.Granted) + { + Console.WriteLine("Currently do not have Location permissions, requesting permissions"); + + var request = await permissionsService.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 async Task GetPositionAsync(TimeSpan? timeout, CancellationToken? cancelToken = null, bool includeHeading = false) + { + 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 tcs; + + var manager = GetManager(); + manager.DesiredAccuracy = DesiredAccuracy; + + // Permit background updates if background location mode is enabled + if (UIDevice.CurrentDevice.CheckSystemVersion(9, 0)) + { + var backgroundModes = NSBundle.MainBundle.InfoDictionary[(NSString)"UIBackgroundModes"] as NSArray; + manager.AllowsBackgroundLocationUpdates = backgroundModes != null && (backgroundModes.Contains((NSString)"Location") || backgroundModes.Contains((NSString)"location")); + } + + // 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(manager); + var singleListener = new GeolocationSingleUpdateDelegate(manager, DesiredAccuracy, includeHeading, timeoutMilliseconds, cancelToken.Value); + manager.Delegate = singleListener; + manager.StartUpdatingLocation(); + + if (includeHeading && SupportsHeading) + manager.StartUpdatingHeading(); + + return await singleListener.Task; + + //tcs = new TaskCompletionSource(); + //if (_lastPosition == null) + //{ + // if (cancelToken != CancellationToken.None) + // cancelToken.Value.Register(() => tcs.TrySetCanceled()); + + // EventHandler gotError = null; + // gotError = (s, e) => + // { + // tcs.TrySetException(new GeolocationException(e.Error)); + // PositionError -= gotError; + // }; + // PositionError += gotError; + + // EventHandler gotPosition = null; + // gotPosition = (s, e) => + // { + // tcs.TrySetResult(e.Position); + // PositionChanged += gotPosition; + // }; + // PositionChanged += gotPosition; + //} + //else + // tcs.SetResult(_lastPosition); + + //return await tcs.Task; + } + + #endregion + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/PermissionsService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/PermissionsService.cs new file mode 100644 index 000000000..6f87b60dc --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/Services/PermissionsService.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using eShopOnContainers.Core.Models.Permissions; +using eShopOnContainers.Core.Services.Permissions; +using CoreLocation; +using UIKit; +using Foundation; + +namespace eShopOnContainers.iOS.Services +{ + public class PermissionsService : IPermissionsService + { + #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 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 authCallback = null; + var tcs = new TaskCompletionSource(); + var 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.Location) + //{ + // if (info.ContainsKey(new NSString("NSLocationAlwaysUsageDescription"))) + // locationManager.RequestAlwaysAuthorization(); + // else 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!"); + //} + //else if (permission == Permission.LocationAlways) + //{ + // if (info.ContainsKey(new NSString("NSLocationAlwaysUsageDescription"))) + // locationManager.RequestAlwaysAuthorization(); + // 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!"); + //} + 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 CheckPermissionStatusAsync(Permission permission) + { + switch (permission) + { + case Permission.LocationWhenInUse: + return Task.FromResult(GetLocationPermissionStatus(permission)); + } + return Task.FromResult(PermissionStatus.Granted); + } + + public async Task> RequestPermissionsAsync(params Permission[] permissions) + { + var results = new Dictionary(); + 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 + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj index 05e42b858..6ea0a4465 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj @@ -126,6 +126,9 @@ + + + @@ -198,12 +201,6 @@ ..\..\..\..\packages\Acr.UserDialogs.6.5.1\lib\Xamarin.iOS10\Acr.UserDialogs.Interface.dll - - ..\..\..\..\packages\Xam.Plugin.Geolocator.3.0.4\lib\Xamarin.iOS10\Plugin.Geolocator.Abstractions.dll - - - ..\..\..\..\packages\Xam.Plugin.Geolocator.3.0.4\lib\Xamarin.iOS10\Plugin.Geolocator.dll - ..\..\..\..\packages\PInvoke.Windows.Core.0.3.2\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\PInvoke.Windows.Core.dll diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/packages.config b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/packages.config index 0669c2fa0..86db43622 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/packages.config +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/packages.config @@ -65,7 +65,6 @@ -