Skip to content

Instantly share code, notes, and snippets.

@salarcode
Last active April 17, 2024 00:00
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save salarcode/da8ad2b993e67c602db88a62259d0456 to your computer and use it in GitHub Desktop.
Save salarcode/da8ad2b993e67c602db88a62259d0456 to your computer and use it in GitHub Desktop.
MAUI Bluetooth Permission Declaration (for Android)
// IMPORTANT: Put this file in `Platforms/Android/` directory.
using Microsoft.Maui.ApplicationModel;
using System.Collections.Generic;
/// <summary>
/// MAUI Bluetooth Permission Decleration (for Android).
/// </summary>
/// <remarks>
/// IMPORTANT: Put this file in `Platforms/Android/` directory.
/// </remarks>
public partial class BluetoothPermissions : Permissions.BasePlatformPermission
{
private readonly bool _scan;
private readonly bool _advertise;
private readonly bool _connect;
private readonly bool _bluetoothLocation;
public BluetoothPermissions()
: this(true, false, true, false)
{
}
/// <summary>
///
/// </summary>
/// <param name="scan">Needed only if your app looks for Bluetooth devices.
/// If your app doesn't use Bluetooth scan results to derive physical location, you can make a strong assertion that your app never uses the Bluetooth permissions to derive physical location.
/// Add the `android:usesPermissionFlags` attribute to your BLUETOOTH_SCAN permission declaration, and set this attribute's value to `neverForLocation`.
/// </param>
/// <param name="advertise">Needed only if your app makes the device discoverable to Bluetooth devices.</param>
/// <param name="connect">Needed only if your app communicates with already-paired Bluetooth devices.</param>
/// <param name="bluetoothLocation">Needed only if your app uses Bluetooth scan results to derive physical location.</param>
/// <remarks>
/// https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
/// </remarks>
public BluetoothPermissions(bool scan = true, bool advertise = true, bool connect = true, bool bluetoothLocation = true)
{
_scan = scan;
_advertise = advertise;
_connect = connect;
_bluetoothLocation = bluetoothLocation;
}
private (string androidPermission, bool isRuntime)[] _requiredPermissions;
public override (string androidPermission, bool isRuntime)[] RequiredPermissions
{
get
{
if (_requiredPermissions != null)
return _requiredPermissions;
var result = new List<(string androidPermission, bool isRuntime)>();
var sdk = (int)Android.OS.Build.VERSION.SdkInt;
if (sdk >= 31)
{
// If your app targets Android 12 (API level 31) or higher, declare the following permissions in your app's manifest file:
if (_scan)
result.Add((global::Android.Manifest.Permission.BluetoothScan, true));
if (_advertise)
result.Add((global::Android.Manifest.Permission.BluetoothAdvertise, true));
if (_connect)
result.Add((global::Android.Manifest.Permission.BluetoothConnect, true));
if (_bluetoothLocation)
result.Add((global::Android.Manifest.Permission.AccessFineLocation, true));
}
else
{
// If your app targets Android 11 (API level 30) or lower, declare the following permissions in your app's manifest file:
result.Add((global::Android.Manifest.Permission.Bluetooth, true));
if (sdk >= 29)
{
result.Add((global::Android.Manifest.Permission.AccessFineLocation, true));
}
else
{
// If your app targets Android 9 (API level 28) or lower, you can declare the ACCESS_COARSE_LOCATION permission instead of the ACCESS_FINE_LOCATION permission.
result.Add((global::Android.Manifest.Permission.AccessCoarseLocation, true));
}
if (_scan || _connect || _advertise)
{
result.Add((global::Android.Manifest.Permission.BluetoothAdmin, true));
if (sdk >= 29)
{
// If your app supports a service and can run on Android 10 (API level 29) or Android 11, you must also declare the ACCESS_BACKGROUND_LOCATION permission to discover Bluetooth devices.
result.Add((global::Android.Manifest.Permission.AccessBackgroundLocation, true));
}
}
}
_requiredPermissions = result.ToArray();
return _requiredPermissions;
}
}
}
/// <summary>
/// IMPORTANT: Put this file in root of Application
/// </summary>
public partial class BluetoothPermissions : Permissions.BasePlatformPermission
{
}
// How to use MAUI Bluetooth LE permissions
private async Task<bool> CheckBluetoothStatus()
{
try
{
var requestStatus = await new BluetoothPermissions().CheckStatusAsync();
return requestStatus == PermissionStatus.Granted;
}
catch (Exception ex)
{
// logger.LogError(ex);
return false;
}
}
public async Task<bool> RequestBluetoothAccess()
{
try
{
var requestStatus = await new BluetoothPermissions().RequestAsync();
return requestStatus == PermissionStatus.Granted;
}
catch (Exception ex)
{
// logger.LogError(ex);
return false;
}
}
@gfmoore
Copy link

gfmoore commented Jun 11, 2022

Hi, Two questions and it's because I'm ignorant.
(EDITED)

  1. I deleted stupid question!

  2. I put the code in my BluetoothPage.xaml.cs code behind but I'm not certain of how to call the task. I'm trying

Task.Run(() => CheckAndRequestBluetoothPermission());  

but I don't know how to test the response? How can I assign the return to something?
e.g var x = Task.Run(() => CheckAndRequestBluetoothPermission());

2b) If I want this to start at application startup where should I put it. I expect I'll only call this once.

There's no guarantee that these aren't stupid questions either :)

Once again thanks so much for your efforts on this, it's a make or break for my app.

@gfmoore
Copy link

gfmoore commented Jun 11, 2022

Sorry I think I'm being dense here.

Also EDITED. Sorry, but when I reviewed what I'd said it didn't look right.

I put all of that in my BluetoothPage.xaml.cs as follows

namespace TSDZ2Monitor.Pages;

public partial class BluetoothPage : ContentPage
{
  public BluetoothPage()
  {
    InitializeComponent();

    Console.WriteLine("Bluetooth here");

    Task.Run(() => CheckBluetoothAccess() );

    Task.Run(() => RequestBluetoothAccess() );

  }

  public async Task<bool> CheckBluetoothAccess()
  {
    try
    {
      var requestStatus = await Permissions.CheckStatusAsync<BluetoothPermissions>();
      return requestStatus == PermissionStatus.Granted;
    }
    catch (Exception ex)
    {
      Console.WriteLine($"Oops  {ex}");
      return false;
    }
  }

  public async Task<bool> RequestBluetoothAccess()
  {
    try
    {
      var requestStatus = await Permissions.RequestAsync<BluetoothPermissions>();
      return requestStatus == PermissionStatus.Granted;
    }
    catch (Exception ex)
    {
      Console.WriteLine($"Oops  {ex}");
      return false;
    }
  }

}

It built and deployed, but I didn't get any popup on my android device? Should I have done? Or is this code something that I need to implement?

I noted that on testing location it showed a very pretty popup, but was this from android or the Maui permissions code?

On my app permissions I now have a Bluetooth permission option, which is set to Notify (the other option being Deny and Always Allow). So it's done something good.

@salarcode
Copy link
Author

salarcode commented Jun 12, 2022

I've got the permissions requirement for each Android version from here:
https://developer.android.com/guide/topics/connectivity/bluetooth/permissions

So if only basic permissions are requested like Scan & Connect and then with a right permissions request there will not be a user prompt. This depends very much on the Android Version and of course vendor. Usually from what I have found out is that Location is the reason for prompts.

Please note that enabled default permissions for BluetoothPermissions are Scan and Connect:

public BluetoothPermissions()
	: this(true, false, true, false)
{ }

Here is how to call from page ContentPage; You just need to call the methods from Loaded event of page, like this; you can make events async:

private async void ContentPage_Loaded(object sender, EventArgs e)
{
	await CheckAndRequestBluetoothPermission();
}

public async Task CheckAndRequestBluetoothPermission()
{
	if (!await CheckBluetoothAccess())
	{
		await RequestBluetoothAccess();
	}
}

... and xaml

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Navigator.MainPage"
			 xmlns:viewmodel="clr-namespace:Navigator.ViewModel"
			 x:DataType="viewmodel:MainPageVm"
			 Loaded="ContentPage_Loaded">
...

@gfmoore
Copy link

gfmoore commented Jun 12, 2022

That's fantastic, thankyou so much. I put a question on stackoverflow about this, so I'll put a link to the github question and here. I guess I can easily add my own prompt popup screen.

I cant tell you how delighted I am about this and I've even managed to get plugin.BLE scanning. With the knowledge gained from your examples I can improve the code below, but at least it works. Long way to go, but now I can make progress. If you have a donate page let me buy you a beer (or a litre of petrol) :)

using Plugin.BLE;
...

    StartScanning();
    static async void  StartScanning()
    {
      var ble = CrossBluetoothLE.Current;
      var adapter = CrossBluetoothLE.Current.Adapter;
      var state = ble.State;

      adapter.DeviceDiscovered += (s, a) => Console.WriteLine(a.Device);
      await adapter.StartScanningForDevicesAsync();
    }

@salarcode
Copy link
Author

I'm glad this was helpful. All good :)
Thanks for letting me know about plugin.BLE

@EJoffre
Copy link

EJoffre commented May 31, 2023

Have you the same example for iOS please ?

@kmccarthyinrule
Copy link

Is there a VS solution with all this together so I can conceptualize it? Thanks in advance!

@axa88
Copy link

axa88 commented Aug 4, 2023

Correct me if im wrong but, it appears to me neither Bluetooth nor BluetoothAdmin are runtime permissions, and thus do not need the runtime parameter to be true.
From what i see in the source code, the only use to even include them in this RequiredPermissions override is to let MAUI ensure they are part of the manifest, but marking them as runtime is misleading/confusing.

Also it seems to me that since CheckStatus/Request permissions only returns a single over all PermissionStatus, than adding both the Bluetooth specific as well as Location specific permissions, to the same RequiredPermissions enumeration, will not allow you to differentiate which permissions if any had not been granted, rather just that something wasn't.
So if you need to know which may have been denied, you wont unless they are requested separately.
For after the permission is denied twice, additional request will not be granted, and arguably you might want to know what exactly was denied to help guide the user to allow the permission manually.

@taylorjrsmith
Copy link

Shoutout to @salarcode , this was of amazing help saved many hours of banging my head against a wall

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment