James Montemagno
James Montemagno

Live, Love, Bike, and Code.

Tags


Twitter


Simplified iOS & Android Runtime Permissions with Plugins!

James MontemagnoJames Montemagno

TLDR; I created a brand new cross-platform Permissions Plugin for Xamarin to enable anyone to query and request common permissions from shared code. You can watch a video here.

Permissions! Blarg!

As an Android developer I have felt pretty spoiled over the last few years when it came to getting access to an API that required a permission. Simply check a checkbox in the project setting and any of the 130+ permissions were mine to have! The user installed my app and automatically granted me every permission my little heart desired. I took a look at what those poor iOS developers (and myself) had to implement in their apps to request a permission, get a callback, and then handle revoking of permissions from users! That looked like no fun at all. Well, Android developers had it too good for too long and now with the release of Android Marshmallow users are back in control with the introduction of Runtime Permissions, and I honestly think it is for the better. However, this means for developers we now have to go off and check permissions on multiple platforms, with different APIs, and no shared code. 

First, let’s take a look at what we would have to do for both iOS and Android to get access to grab the users location.

iOS:

  1. Add NSLocationWhenInUseUsageDescription or  NSLocationAlwaysUsageDescription to info.plist.
  2. Create new CLLocatonManager
  3. Check we have the permission
  4. Request correct permission if needed
  5. Register for callbacks when authorization changes

Here is what it would look like:


void RequestLocationPermission()
{

    var locationManager = new CLLocationManager();

    EventHandler authCallback = null;

    authCallback = (sender, e) =>
        {
            if(e.Status == CLAuthorizationStatus.NotDetermined)
                return;
            
            locationManager.AuthorizationChanged -= authCallback;
            //do stuff here 
        };

    locationManager.AuthorizationChanged += authCallback;


    var info = NSBundle.MainBundle.InfoDictionary;
    if (info.ContainsKey(new NSString("NSLocationWhenInUseUsageDescription")))
        locationManager.RequestWhenInUseAuthorization();
    else 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!");
}

Android:

  1. Add correct permissions to AndroidManifest
  2. Check permissions on API 23+
  3. Check if need to show rationale (user revoked or denied)
  4. Request permissions
  5. Do this every single time before you make a call!

Here is what that would look like:


async Task GetLocationCompatAsync()
{
  const string permission = Manifest.Permission.AccessFineLocation;
  if (ContextCompat.CheckSelfPermission(this, permission) == (int)Permission.Granted)
  {
    await GetLocationAsync();
    return;
  }

if (ActivityCompat.ShouldShowRequestPermissionRationale(this, permission))
{
//Explain to the user why we need to read the location
Snackbar.Make(layout, "Location access is required to show coffee shops nearby.", Snackbar.LengthIndefinite)
.SetAction("OK", v => ActivityCompat.RequestPermissions(this, PermissionsLocation, RequestLocationId))
.Show();

return;

}

ActivityCompat.RequestPermissions(this, PermissionsLocation, RequestLocationId);
}

The Problem

It is pretty clear that this code is pretty tedious to write and it has to be done in the actual iOS or Android projects, which is a complete bummer. While we have been forced to write this code for years, I say NO MORE! Last week I set out on a goal of creating my next Plugins for Xamarin to solve Permissions once and for all. Here was the goal:

  1. Simple Cross-platform Async API that should:
  2. CheckPermisssion - Simply check the permission
  3. ShouldShowRationale - Tell us if the user revoked access on Android
  4. RequestPermissions - Any amount that you would like!
  5. Work on a wide array of iOS and Android permissions that require runtime checks

Today, I am pleased to introduce my latest Plugin that does just this, Permissions Plugin for Xamarin! This will not only simplify permissions on a per platform basis, but also from shared code such as Xamarin.Forms. More importantly though it will enable Library creators to deeply integrate with the SDKs without having to worry about writing all the permission code. Did I mention it is all async??!?!?! 

Permissions I Handle

The nice thing is that there are just 9 “permission groups”  (about 30+ permissions) on Android that have to be checked and about the same on iOS currently. Here is a full listing of everything the library can do:

  1.   Calendar (Android: Calendar, iOS: Events)
  2.   Camera (Android: Camera, iOS: Camera Roll & Camera)
  3.   Contacts (Android: Contacts, iOS: AddressBook)
  4.   Location (Android: Location, iOS: CoreLocation (Always/WhenInUse))
  5.   Microphone (Android: Microphone, iOS: Microphone)
  6.   Phone (Android: Phone, iOS: Nothing)
  7.   Photos (Android: Nothing, iOS: Photos)
  8.   Reminders(Android: Nothing, iOS: Reminders)
  9.   Sensors (Android: Body Sensors, iOS: CoreMotion)
  10.   Sms (Android: Sms, iOS: Nothing)
  11.   Storage (Android: External Storage, iOS: Nothing)

Implementation

All in all I have to say that iOS permission requesting is pretty straight forward. You just have to go in aware that each library has its own way of handling permissions, different enums, and different callbacks to handle. If you read through the documentation for each of these APIs you will see that they are pretty identical except that you have different types of status: AVAuthorizationStatus, ABAuthorizationStatus, EKAuthorizationStatus, CLAuthorizationStatus, and more! Each of these actually have a common type though of Granted, Denied, Restricted, or Unknown. I decided copy most of these when returning a PermissionStatus to you:

  1. Denied
  2. Disabled (feature is disabled, such as locatio)
  3. Granted
  4. Restricted
  5. Unknown

To hand the fact that there is a mixture of Async calls to check the status of these permissions and no way to actually request them async I simply used a TaskCompletionSource on every single call such as:


Task RequestPhotosPermission()
{
    if (PhotosPermissionStatus == PermissionStatus.Granted)
        return Task.FromResult(PermissionStatus.Granted);

    var tcs = new TaskCompletionSource();

    PHPhotoLibrary.RequestAuthorization(status =>
        {
            switch(status)
            {
                case PHAuthorizationStatus.Authorized:
                    tcs.SetResult(PermissionStatus.Granted);
                    break;
                case PHAuthorizationStatus.Denied:
                    tcs.SetResult(PermissionStatus.Denied);
                    break;
                case PHAuthorizationStatus.Restricted:
                    tcs.SetResult(PermissionStatus.Restricted);
                    break;
                default:
                    tcs.SetResult(PermissionStatus.Unknown);
                    break;
            }
        });

    return tcs.Task;
}


Android on the other hand is a bit more tricky. To check a permission this can be done on the activity or the application context, which is what I will use if you are trying this on a background service.

To request a permission you actually need an Activity and then you have to actually override a method on the activity to handle the request. This is obviously an issue because I am writing a library and can’t access the Activity you are on. Well, that used to be the case until I created the CurrentActivity plugin! Enabling you, me, or any library creator to get access to the current activity! 

This means a new MainApplication.cs file will get installed when you install the plugin, so be sure to read all about that! Now that I have the Activity the next issue to overcome is that fact that the API I created takes in my enum of a permission group, but Android requires you request all of the Manifest permissions you have specified for that group. This means that yes you will still need to add the permissions to your manifest, but I do a reverse lookup to see what you have listed in the AndroidManifest and those are what I request! What!!!! Amazing! I know! However, there is one small caveat here, which is whatever Activity you are requesting from you will have to notify my library when the call is returned by simply adding the following lines of code:


public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
PermissionsImplementation.Current.OnRequestPermissionsAsyncResult(requestCode, permissions, grantResults);
}

If you are supporting more than iOS/Android and have a bunch of Windows apps, do not fear as I added in dummy implementations for all flavors of Windows and .NET so permissions will return true.

In Action

Finally I get to just show you the code! It is actually super straight forward. Let’s say you want to request the location using the Geolocator library (although it does check permissions in the upcoming version 2.0.0).  Here are the calls in a PCL (Xamarin.Forms app hence the DisplayAlerts):


try
{
    var status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Location);
    if (status != PermissionStatus.Granted)
    {
        if(await CrossPermissions.Current.ShouldShowRequestPermissionRationaleAsync(Permission.Location))
        {
            await DisplayAlert("Need location", "Gunna need that location", "OK");
        }

        var results = await CrossPermissions.Current.RequestPermissionsAsync(new[] {Permission.Location});
        status = results[Permission.Location];
    }

    if (status == PermissionStatus.Granted)
    {
        var results = await CrossGeolocator.Current.GetPositionAsync(10000);
        LabelGeolocation.Text = "Lat: " + results.Latitude + " Long: " + results.Longitude;
    }
    else if(status != PermissionStatus.Unknown)
    {
        await DisplayAlert("Location Denied", "Can not continue, try again.", "OK");
    }
}
catch (Exception ex)
{

    LabelGeolocation.Text = "Error: " + ex;
}

Grab the NuGet & Code

Last week I pushed all the code up to GitHub and made the NuGet packages available for all. I will be integrating the Permissions plugin into all of my other Plugins that require permission checking. I also recorded a Motz Codes Live with a full walkthrough.

Live, Love, Bike, and Code

Comments