Skip to content
This repository has been archived by the owner on May 15, 2024. It is now read-only.

GH-7: Geolocation API's #132

Merged
merged 43 commits into from
Apr 10, 2018
Merged

GH-7: Geolocation API's #132

merged 43 commits into from
Apr 10, 2018

Conversation

Redth
Copy link
Member

@Redth Redth commented Mar 29, 2018

Description of Change

Adds Geolocation API's

API Changes

public static Task<Location> GetLastKnownLocationAsync();
public static Task<Location> GetLocationAsync();
public static Task<Location> GetLocationAsync(GeolocationRequest request);
public static Task<Location> GetLocationAsync(GeolocationRequest request, CancellationToken cancelToken);
public enum GeolocationAccuracy
{
    // iOS:     ThreeKilometers         (3000m)
    // Android: ACCURACY_LOW, POWER_LOW (500m)
    // UWP:     3000                    (1000-5000m)
    Lowest,

    // iOS:     Kilometer               (1000m)
    // Android: ACCURACY_LOW, POWER_MED (500m)
    // UWP:     1000                    (300-3000m)
    Low,

    // iOS:     HundredMeters           (100m)
    // Android: ACCURACY_MED, POWER_MED (100-500m)
    // UWP:     100                     (30-500m)
    Medium,

    // iOS:     NearestTenMeters        (10m)
    // Android: ACCURACY_HI, POWER_MED  (0-100m)
    // UWP:     High                    (<=10m)
    High,

    // iOS:     Best                    (0m)
    // Android: ACCURACY_HI, POWER_HI   (0-100m)
    // UWP:     High                    (<=10m)
    Best
}
public class GeolocationRequest
{
    public GeolocationRequest();
    public GeolocationRequest(GeolocationAccuracy accuracy);
    public GeolocationRequest(GeolocationAccuracy accuracy, TimeSpan timeout);
    
    public TimeSpan? Timeout { get; set; }
    public GeolocationAccuracy DesiredAccuracy { get; set; }
}

PR Checklist

  • Has tests (if omitted, state reason in description)
  • Has samples (if omitted, state reason in description)
  • Rebased on top of master at time of PR
  • Changes adhere to coding standard
  • Updated documentation (see walkthrough)

@Redth Redth changed the title Geolocation API's [WIP] Geolocation API's Mar 29, 2018
@Redth Redth added the DO-NOT-MERGE Don't merge it.... don't do it! label Mar 29, 2018
@Redth Redth self-assigned this Mar 29, 2018
@Redth Redth added this to the Preview-1 milestone Mar 29, 2018
@mattleibow mattleibow changed the title [WIP] Geolocation API's GH-7: Geolocation API's Mar 29, 2018
@mattleibow mattleibow self-assigned this Apr 5, 2018
@@ -9,6 +9,10 @@ namespace Microsoft.Caboodle
{
internal static partial class Permissions
{
static readonly object locationLocker = new object();
static CLLocationManager locationManager;
static TaskCompletionSource<PermissionStatus> locationTcs;
Copy link
Contributor

@mattleibow mattleibow Apr 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a set of eyes here 👀 - and on all the changes to make the permissions not just disappear in a second.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am locking and making sure that there is only a single location manager for all permissions, but is this necessary?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also saw a constructor for TaskCompletionSource that takes object state... I have never used that, but maybe there is some way to use that to keep the listener alive... will investigate, but comments will help.

@@ -31,5 +32,18 @@ internal static string Md5Hash(string input)
return hash.ToString();
#endif
}

internal static CancellationToken TimeoutToken(CancellationToken cancellationToken, TimeSpan timeout)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A util function that will auto-cancel a token after a timeout

{
await Permissions.RequireAsync(PermissionType.LocationWhenInUse).ConfigureAwait(false);

var manager = new CLLocationManager();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This location manager is different to the one used in permissions. Need to review if this gets GC'ed unexpectedly.


var manager = new CLLocationManager();

var tcs = new TaskCompletionSource<CLLocation>(manager);
Copy link
Contributor

@mattleibow mattleibow Apr 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep a reference to the manager for the duration of the task... this needs 👀 and 💭

var locationManager = new CLLocationManager();
var manager = new CLLocationManager();

var tcs = new TaskCompletionSource<PermissionStatus>(manager);
Copy link
Contributor

@mattleibow mattleibow Apr 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep a reference to the manager for the duration of the task... this needs 👀 and 💭

@mattleibow
Copy link
Contributor

mattleibow commented Apr 5, 2018

With regards to a fail-fast case, this is a bit harder to implement. To be realistic, almost all devices nowadays have a GPS or some basic location system. In most cases there are 2 alternatives: no permission and disabled.

UWP is fun with a more async model. The locator has to init and then updates a status property during that process. Also, when the GPS is off, the RequestAccessAsync returns Denied. In addition, we have to first get permission and then attach to an event to watch for the status to become favourable or not. In fact, if you request location (with GetGeopositionAsync), and then disable the GPS, an UnauthorizedAccessException is thrown.

iOS appears to always have a GPS, and they have a property for detecting whether it enabled or not.

Android appears to be like UWP in that you have to get permission in order to see what GPS/location devices are available. If there are no device available, then all have been disabled and thus no location data. If there are no devices, it could mean that all are disabled, or that there just simply are none.

@mattleibow
Copy link
Contributor

Just looking at the implementations, we have to handle the cases of the location devices being disabled or permissions revoked during a request.

@Redth
Copy link
Member Author

Redth commented Apr 5, 2018

Honestly this is more reason to consider play services location since it helps deal with getting the user to enable location. Not to mention it's a much better API to deal with. I'm not excited about the dependency it would add and we would still need to fall back on the old api but something to consider!

@jamesmontemagno
Copy link
Collaborator

I have a version 5 of the geolocator plugin, it does help a bit on Android that is sure, but yeah some tricky parts with google play as a dependency :(

What I did on Android was detect if it had any providers available to determine if it was active or not. The gotcha is that you need to have permissions set to query providers :)

iOS is easy peasy:

 public bool IsGeolocationEnabled
        {
            get
            {         
                var status = CLLocationManager.Status;
                return CLLocationManager.LocationServicesEnabled;
            }
        }

UWP is a bundle of terrible to be honest with you see here: jamesmontemagno/GeolocatorPlugin#144

@jamesmontemagno
Copy link
Collaborator

I am wondering if it is best just to assume always on... make the request and throw the exception if it fails... we are exception first.. the question is does that work properly.

On iOS/Android we can check permissions first... UWP is a pain in the butt a bit.


static async Task<Location> PlatformLastKnownLocationAsync()
{
await Permissions.RequireAsync(PermissionType.LocationWhenInUse).ConfigureAwait(false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the configureawait for now throughout. I am still worried about that stuff.

var manager = new CLLocationManager();
var location = manager.Location;

return new Location
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this an extension method to convert like i do for geocoding. It may already be there actually.

{
try
{
return new DateTimeOffset((DateTime)timestamp);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should put these in platform helpers

if (location?.Coordinate == null)
return null;

return new Location
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here for extension methods

@@ -84,6 +85,9 @@ public static void BeginInvokeOnMainThread(Action action)

internal static ClipboardManager ClipboardManager =>
Application.Context.GetSystemService(Context.ClipboardService) as ClipboardManager;

internal static LocationManager LocationManager =>
(LocationManager)Application.Context.GetSystemService(Context.LocationService);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use "as" for null checks along the way.

@@ -53,5 +53,7 @@
<string>Caboodle.Samples</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Access to your location is required for cool things to happen!</string>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tab this puppy over

@Redth
Copy link
Member Author

Redth commented Apr 5, 2018

Yeah I say we just check for hardware and not try and query enabled status here. I think it's not expected to have a UI asking for permission when checking if it's supported. Also we don't really expose the is supported yet anyway so there really is no code path that wouldn't check for permission anyway.

Tl;dr let's check for hardware only.

@mattleibow
Copy link
Contributor

mattleibow commented Apr 9, 2018

There is something I noticed on iOS is that the test will fail because the location manager has some arb requirements about where locations are started from:

https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate
The methods of your delegate object are called from the thread in which you started the corresponding location services. That thread must itself have an active run loop, like the one found in your application’s main thread.

If the location is requested from a background thread, then the task will wait indefinitely since there is no feedback whatsoever.

I have updated the tests to call the location manager from the UI thread and I throw an exception in the request code, but this requirement may/may not be acceptable for us. There are some steps that we can take:

  1. just keep the exception for iOS only
    a bit nasty, but super easy
  2. automatically jump to the main/UI thread
    this will cause confusion since the task was called from a background thread, but now we are on a new thread
  3. call from the main thread, and then marshal back to the original thread
    this will make the threading appear normal, but there may be consequences later on

@mattleibow
Copy link
Contributor

I went with option 3.

// so just use the main loop
Platform.BeginInvokeOnMainThread(() =>
{
var manager = new CLLocationManager();
Copy link
Contributor

@mattleibow mattleibow Apr 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We create and start the location manger on the main thread:

  • because we need an active run loop
  • it is non-blocking
  • it does not affect the actual call threading system

If we are on the main thread already, this basically is just a straight execution as if nothing special was happening.

});

// still wait and return on the background thread
var clLocation = await tcs.Task;
Copy link
Contributor

@mattleibow mattleibow Apr 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We await the location manager:

  • we still wait the current thread until the main thread gets back to us
  • we continue on the background thread

@mattleibow
Copy link
Contributor

build

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FLASHLIGHT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need Read/Write for anything?

{
try
{
locationManager.RemoveUpdates(listener);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we really ignore this?

break;
}

return locationManager.GetBestProvider(criteria, true) ?? locationManager.GetProviders(true).First();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be FirstorDefault?

{
await Permissions.RequireAsync(PermissionType.LocationWhenInUse);

var manager = new CLLocationManager();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have this on the main thread or doesn't matter??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not appear to matter.


wasRaised = true;

var location = locations.LastOrDefault();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we assuming these are ordered by priority? Accuracy?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the docs:

The objects in the array are organized in the order in which they occurred. Therefore, the most recent location update is at the end of the array.
https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate/1423615-locationmanager

@jamesmontemagno jamesmontemagno merged commit f185ef4 into master Apr 10, 2018
@mattleibow mattleibow deleted the geolocation branch April 10, 2018 19:28
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
DO-NOT-MERGE Don't merge it.... don't do it!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants