To use Bluetooth Low Energy in an Android App it is necessary to include multiple permissions in the Manifest and a runtime check the user has approved these permissions
The issue is there are 6 possible permissions but attempting to check all the permissions will fail! This is because runtime permissions are never granted for some versions of Android.
The 6 permissions are
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
A big thank you to @Kozmotronik for the simple solution of only checking permissions required by each version of Android
The following checks the required permissions and if not already granted adds to a list. It then requests the missing permissions.
private fun checkAndRequestMissingPermissions() {
// check required permissions - request those which have not already been granted
val missingPermissionsToBeRequested = ArrayList<String>()
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.BLUETOOTH)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.BLUETOOTH_ADMIN)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// For Android 12 and above require both BLUETOOTH_CONNECT and BLUETOOTH_SCAN
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.BLUETOOTH_CONNECT)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.BLUETOOTH_SCAN)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// FINE_LOCATION is needed for Android 10 and above
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.ACCESS_COARSE_LOCATION)
}
if (missingPermissionsToBeRequested.isNotEmpty()) {
writeToLog("Missing the following permissions: $missingPermissionsToBeRequested")
ActivityCompat.requestPermissions(this, missingPermissionsToBeRequested.toArray(arrayOfNulls<String>(0)), REQUEST_MULTIPLE_PERMISSIONS)
} else {
writeToLog("All required permissions GRANTED !")
}
}
onRequestPermissionsResult() can then check the user has approved the required permissions
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_MULTIPLE_PERMISSIONS) {
for (idx in permissions.indices) {
var result = if (grantResults[idx] == PackageManager.PERMISSION_GRANTED) "Granted" else "Denied"
writeToLog("REQUEST_MULTIPLE_PERMISSIONS Permission ${permissions[idx]} $result}")
}
}
}
Android requires a permission check before giving access to some BLE functions. Where the permission will not be granted it must still be checked, but can be made to work by adding a check of the Android version.
For example
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
writeToLog("Scan permission denied")
} else {
bluetoothScanner.startScan(filters, scanSettings, scanCallback)
writeToLog("Bluetooth Scan Started")
}
Recently @Kozmotronik has improved his answer which is shown below
Google made Android more strict by adding the new BLUETOOTH_CONNECT
and BLUETOOTH_SCAN
permissions. You will get SecurityException
in runtime if you attempt to access any BLE API that requires these permissions. So we need to check the permissions in the activity which is set to android.intent.action.MAIN
in the manifest file. I call that as MainActivity
. Unfortunately I don't code in Kotlin yet, so I will write the examples in Java.
MainActivity
public class MainActivity extends AppCompatActivity {
private static int BLE_PERMISSIONS_REQUEST_CODE = 0x55; // Could be any other positive integer value
private int permissionsCount;
//...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Other codes...
checkBlePermissions();
// Maybe some more codes...
}
// Maybe some other codes...
private String getMissingLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// COARSE is needed for Android 6 to Android 10
return Manifest.permission.ACCESS_COARSE_LOCATION;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// FINE is needed for Android 10 and above
return Manifest.permission.ACCESS_FINE_LOCATION;
}
// No location permission is needed for Android 6 and below
return null;
}
private boolean hasLocationPermission(String locPermission) {
if(locPermission == null) return true; // An Android version that doesn't need a location permission
return ContextCompat.checkSelfPermission(getApplicationContext(), locPermission) ==
PackageManager.PERMISSION_GRANTED;
}
private String[] getMissingBlePermissions() {
String[] missingPermissions = null;
String locationPermission = getMissingLocationPermission();
// For Android 12 and above
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if(ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN)
!= PackageManager.PERMISSION_GRANTED) {
missingPermissions = new String[1];
missingPermissions[0] = Manifest.permission.BLUETOOTH_SCAN;
}
if(ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT)
!= PackageManager.PERMISSION_GRANTED) {
if (missingPermissions == null) {
missingPermissions = new String[1];
missingPermissions[0] = Manifest.permission.BLUETOOTH_CONNECT;
} else {
missingPermissions = Arrays.copyOf(missingPermissions, missingPermissions.length + 1);
missingPermissions[missingPermissions.length-1] = Manifest.permission.BLUETOOTH_CONNECT;
}
}
}
else if(!hasLocationPermission(locationPermission)) {
missingPermissions = new String[1];
missingPermissions[0] = getMissingLocationPermission();
}
return missingPermissions;
}
private void checkBlePermissions() {
String[] missingPermissions = getMissingBlePermissions();
if(missingPermissions == null || missingPermissions.length == 0) {
Log.i(TAG, "checkBlePermissions: Permissions is already granted");
return;
}
for(String perm : missingPermissions)
Log.d(TAG, "checkBlePermissions: missing permissions "+perm);
permissionsCount = missingPermissions.length;
requestPermissions(missingPermissions, BLE_PERMISSIONS_REQUEST_CODE);
}
// Maybe some other codes...
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if(requestCode == BLE_PERMISSIONS_REQUEST_CODE) {
int index = 0;
for(int result: grantResults) {
if(result == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Permission granted for "+permissions[index]);
if(permissionsCount > 0) permissionsCount--;
if(permissionsCount == 0) {
// All permissions have been granted from user.
// Here you can notify other parts of the app ie. using a custom callback or a viewmodel so on.
}
} else {
Log.d(TAG, "Permission denied for "+permissions[index]);
// TODO handle user denial i.e. show an informing dialog
}
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
We are done with requesting the all needed permissions depending on the device's SDK so far. Now we need to block the API call pathways to be able to check the permissions. Somewhere where you implement bluetooth scanning put a function like startScanning()
as in the code example below, instead of using the BleScanner.scan()
API directly. All the following functions must be in the same activity or fragment.
Another activity or fragment where scanning is implemented
private String getMissingLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// COARSE is needed for Android 6 to Android 10
return Manifest.permission.ACCESS_COARSE_LOCATION;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// FINE is needed for Android 10 and above
return Manifest.permission.ACCESS_FINE_LOCATION;
}
// No location permission is needed for Android 6 and below
return null;
}
private boolean hasLocationPermission() {
String missingLocationPermission = getMissingLocationPermission();
if(missingLocationPermission == null) return true; // No permissions needed
return ContextCompat.checkSelfPermission(requireContext(), missingLocationPermission) ==
PackageManager.PERMISSION_GRANTED;
}
private boolean checkLocationService(@Nullable Runnable r) {
boolean locationServiceState = isLocationServiceEnabled();
String stateVerbose = locationServiceState ? "Location is on" : "Location is off";
Log.d(TAG, stateVerbose);
if(!locationServiceState){
new MaterialAlertDialogBuilder(requireContext())
.setCancelable(false)
.setTitle("Location Service Off")
.setView("Location service must be enabled in order to scan the bluetooth devices.")
.setPositiveButton(android.R.string.ok, (dialog, which) ->
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)))
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
if(r != null) r.run();
})
.create().show();
}
return locationServiceState;
}
private boolean isLocationServiceEnabled(){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
// This is provided as of API 28
LocationManager lm = (LocationManager) requireContext().getSystemService(Context.LOCATION_SERVICE);
return lm.isLocationServiceEnabled();
} else {
// This is deprecated as of API 28
int mod = Settings.Secure.getInt(requireContext().getContentResolver(), Settings.Secure.LOCATION_MODE,
Settings.Secure.LOCATION_MODE_OFF);
return (mod != Settings.Secure.LOCATION_MODE_OFF);
}
}
private void startScanning() {
// Here we intervene the scanning process and check whether the user allowed us to use location.
if(!hasLocationPermission()) {
// Here you have to request the approprite location permission similar to that main activity class
return;
}
// Location service must be enabled
if(!checkLocationService(() -> // Pass a Runnable that starts scanning)) return;
// Everything is good, CAN START SCANNING
}
This logic is a little confusing by nature but it is robust and have been running in a real application that presents in Google Play Store. However It is not 100% complete code since you need to adapt the idea behind it to your application.