I found that adding just the permission in the manifest was not sufficient. I had to provide an option for the user to grant it at start up, just like I need to do for the ACCESS_FINE_LOCATION. Interestingly, when I check if the background permission is already granted in the app, it never gives false. But if I don't check, I dont get the "Allow all the time" option. This is on an Android Pixel 2 version 10
Its only necessary in the 10+ versions of Android. Requesting both permissions gives the user the choice in the dialog box ... of course the user may not select 'All the time'. There is nothing one can do about that!
I am a little embarrassed to add my code because I know it sucks. Someone else could surely improve it. I could not find a way to avoid cycling through the permissions twice.
/**
* This method lists all the permissions needed and gives reasons for each of them in a single
* dialog. When the user presses ok, Android will popup separate dialogs for each permission
* that is needed. In the case of location, the "Allow all the time" gives background permission
* "Allow only while the app is running gives just the location permission while in foreground.
* @return false if permissions are to be requested, true if they have already been granted.
*
* In Android 11 Build 30 things are a lot different. One has to ask for location first, approve
* the 'while the app is running' option and then upon return in the Activity result, ask for
* the background. Thus the new variable 'secondPassR' to handle this case. After the background
* one comes back to the Activity result and then one can go into the main activity.
*/
static boolean secondPassR = false;
private static final int REQUEST_CODE_MULTIPLE_PERMISSIONS = 57;
private boolean requestPermissions(Activity activity)
{
Log.v(TAG, "requestPermissions() called");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
{
final List<String> permissionsList = new ArrayList<>();
final List<String> reasonList = new ArrayList<>();
if(!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION, activity))
{
reasonList.add("LOCATION PERMISSION: Needs needs to be granted for the Bluetooth " +
" LE scanner to discover devices!\n\n");
}
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) && (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) || secondPassR)
{
if (!addPermission(permissionsList, Manifest.permission.ACCESS_BACKGROUND_LOCATION, activity))
{
reasonList.add("BACKGROUND PERMISSION: Needs to be granted for the Bluetooth " +
"LE Scanner to run in the background.\n\n");
}
}
if (permissionsList.size() > 0)
{
if (reasonList.size() > 0)
{
// Need Rationale
StringBuilder message = new StringBuilder(reasonList.get(0));
for (int i = 1; i < reasonList.size(); i++)
{
message.append(" ").append(reasonList.get(i));
}
final androidx.appcompat.app.AlertDialog.Builder builder =
new androidx.appcompat.app.AlertDialog.Builder(new ContextThemeWrapper(activity, R.style.Theme_AppCompat_Light));
builder.setTitle("Demo needs the following permissions:");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
{
builder.setMessage(Html.fromHtml(message.toString(), Html.FROM_HTML_MODE_LEGACY));
}
else
{
builder.setMessage(Html.fromHtml(message.toString()));
}
builder.setPositiveButton(android.R.string.ok, null);
builder.setOnDismissListener(dialog -> {
Log.v(TAG, "Requesting permissions");
activity.requestPermissions(permissionsList.toArray(new String[0]), // newer Java recommended
REQUEST_CODE_MULTIPLE_PERMISSIONS);
});
builder.show();
return false;
}
activity.requestPermissions(permissionsList.toArray(new String[0]), // newer Java recommended
REQUEST_CODE_MULTIPLE_PERMISSIONS);
}
else
{
return true;
}
}
else
{
return true;
}
return false;
}
@TargetApi(23)
private boolean addPermission(List<String> permissionsList, String permission, Activity activity)
{
Log.v(TAG,
"addPermission() called with: " + "permissionsList = " +
"[" + permissionsList + "], permission = [" + permission + "]");
if (ActivityCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED)
{
permissionsList.add(permission);
// Check for Rationale Option
return activity.shouldShowRequestPermissionRationale(permission);
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
{
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
String permission = "";
Log.v(TAG,
"onRequestPermissionsResult() called with: " + "requestCode = [" + requestCode +
"], permissions = [" + Arrays.toString(permissions) + "]," +
" grantResults = [" + Arrays.toString(grantResults) + "]");
if (requestCode == REQUEST_CODE_MULTIPLE_PERMISSIONS)
{
for (int i = 0; i < permissions.length; i++)
{
switch (permissions[i])
{
case Manifest.permission.ACCESS_FINE_LOCATION:
if (grantResults[i] == PackageManager.PERMISSION_GRANTED)
{
Log.d(TAG, "H@H: onRequestPermissionsResult: FINE LOCATION PERMISSION");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
{
Log.d(TAG, "H@H: Now requesting BACKGROUND PERMISSION for version 11+");
secondPassR = true;
requestPermissions(thisActivity);
return;
}
}
break;
case Manifest.permission.ACCESS_BACKGROUND_LOCATION:
if (grantResults[i] == PackageManager.PERMISSION_GRANTED)
{
Log.d(TAG, "H@H: onRequestPermissionsResult: BACKGROUND PERMISSION");
}
break;
}
}
}
Log.d(TAG, "Starting primary activity");
secondPassR = false;
startActivityForResult(new Intent(context, PchaDemoPhg_Activity.class), EXIT_QUIT);
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener)
{
new AlertDialog.Builder(this)
.setMessage(message)
.setCancelable(false)
.setPositiveButton("OK", okListener)
.create()
.show();
}
============== UPDATE ANDROID 12 AND ActivityResultLauncher =======
With Android 12 a new set of permissions appear and we no longer have to ask for location permissions to use a BTLE scanner. THey also have a new way to handle activity results and that is the ActivityResultLauncher, and one of the built in features is for runtime permissions. Below is what I now use to request Bluetooth scanner and background permissions from ANdroid 6+. I use the single-permission launcher to place an explanation dialog before each request. The user experience is far better than what I used to have. (I include my string resources.)
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.util.Log;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.app.ActivityCompat;
import java.util.ArrayList;
import java.util.List;
/*
resource strings used:
<string name="permissions_connect_12"><p>This app needs permission to connect to Bluetooth devices</p> </string>
<string name="permissions_scan_12"><p>To search for Bluetooth devices this app needs permission to use the BLE scanner.</p> </string>
<string name="permissions_overlay_11">READ CAREFULLY! Setting permissions in Android 11+ is much more complicated!\n\n
This app wants to popup a dialog when it discovers a PHD that it can work with.\n\n
It needs Window-Overlay permissions to do that. Android will start a Settings system activity where it will list all the installed applications.\n\n
You will need to scroll down to Health@Home PHG and tap it. That will bring you to the original pre-11 Settings system overlay permission activity.
Give the permission and use the back arrow to exit. You will need use the back arrow once more to return to Health@Home.</string>
<string name="permissions_location"><p><font color=\"#007700\"><b>LOCATION PERMISSION:</b></font> Needs to be granted in order for this app to use the Bluetooth LE scanner.
The scanner is needed to discover BLE health devices and know what they are.</p>
<p><font color=\"red\"><b>This app does NOT use location information or expose location information!</b></font>
Without this permission you will only be able to work with SPP and HDP devices.</p></string>
<string name="permissions_background"><p>BACKGROUND PERMISSION: Needs to be granted for the Bluetooth LE Scanner to run in the background
to support re-connection without user intervention.</p>
<p>Please select \'Allow all the time\'.</p></string>
*/
public class PermissionsActivity extends AppCompatActivity
{
private final static String TAG = PermissionsActivity.class.getName();
private final static boolean isVersionS_Plus = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S); // 31
private final static boolean isVersionM_Plus = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M); // 23 (Marshmallow)
private final static boolean isVersionN_Plus = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N);
private static final boolean isVersionO_Plus = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
private static final boolean isVersionR = (Build.VERSION.SDK_INT == Build.VERSION_CODES.R); // 30
private static final boolean isVersionR_Plus = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R); // 30
private static final boolean isVersionOtoR = ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) // 26
&& (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R)); // 30
private final List<String> permissionsList = new ArrayList<>();
private final List<String> reasonList = new ArrayList<>();
private int index = 0; // Keeps track of what permission is being requested
private int indexMax; // The maximum number of permissions being asked for (set below)
ActivityResultLauncher<String> activityResultLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback<Boolean>()
{
@Override
public void onActivityResult(Boolean result)
{
Log.d(TAG, "HH2: Permission " + permissionsList.get(index) + (result ? " granted" : "rejected"));
index++;
if (index >= indexMax)
{
handlePermissionSummary();
}
else
{
requestPermissions(index);
}
}
});
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// too lazy to make a layout; your activity very likely has one!
if (handlePermissions())
{
//setLoginScreen(); // Do what needs to be done after permissions are granted or if they already are granted
}
}
// Method checks to see if the needed permissions are granted and if they are it returns true.
// If they are not granted the method returns false but starts the process of asking the user to grant the permissions
@SuppressLint("NewApi")
private boolean handlePermissions()
{
if (isVersionS_Plus) // Android 12 + (31+) Completely new runtime permissions for Bluetooth in Android 12
{ // At least one no longer has to ask for location permissions for Bluetooth completely
// confusing the user.
// Requesting BLUETOOTH_CONNECT, BLUETOOTH_SCAN, and ACCESS_BACKGROUND_LOCATION. The latter is still needed
// to use the BTLE scanner in the background.
// There is a weird bug in Android 12 with respect to the BLUETOOTH_CONNECT and BLUETOOTH_SCAN
// permissions. If you need both, regardless of what order you ask for them in, you get only one
// dialog from Android where the user grants the permission. But you have to ask for both permissions
// (if you need both). See this bug: https://issuetracker.google.com/issues/214434187
// Thus I skip the application dialog by making it empty for the second permission and just request
// the permission. That way both permissions get granted.
if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED)
{
permissionsList.add(Manifest.permission.BLUETOOTH_CONNECT);
reasonList.add(getString(R.string.permissions_connect_12));
}
if (checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
{
permissionsList.add(Manifest.permission.BLUETOOTH_SCAN);
reasonList.add(""); // Work-a-round. If empty, present no dialog explaining the request to the user
//reasonList.add(getString(R.string.permissions_scan_12));
}
if (checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED)
{
permissionsList.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
reasonList.add(getString(R.string.permissions_background));
}
indexMax = 3; // Need three permissions
}
else if (isVersionM_Plus)
{
// Need location permissions to use the BTLE Scanner. Some versions of Android after 6 require FINE location and
// some require only coarse and some both. TO minimize headache, I always ask for FINE and place COARSE location
// in the Manifest file. That gives you use of the BTLE scanner for all pre-12 versions of Android
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) // Android 6 +
{
permissionsList.add(Manifest.permission.ACCESS_FINE_LOCATION); // Require ACCESS_COARSE_LOCATION in Manifest file as well
reasonList.add(getString(R.string.permissions_location));
indexMax = 1; // Need only one here
}
if (isVersionOtoR) // Android 8 - 11. For these versions need BACKGROUND permission to use the scanner in the background.
{
if (checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED)
{
permissionsList.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
reasonList.add(getString(R.string.permissions_background));
indexMax = 2; // Need two permissions here
}
}
}
// If no permissions are needed, return true.
if (permissionsList.size() == 0)
{
return true;
}
// Otherwise, begin the permission request sequence which involves launching permission requests.
// The process is asynchronous, so the launch returns immediately.
requestPermissions(index);
return false; // returning false indicates that permissions have not been granted and the request process is ongoing
}
// THis method pops up an application dialog explaining to the user why the application needs the requested permission.
// When the user clicks OK, the permission request is launched. Android pops up whatever system action it dreams up to
// handle the request. Sometimes it is a dialog, and sometimes it is an activity.
// After the user responds, the result is returned in ActivityResultCallback above. The result is a boolean - true if
// granted, false if not. Within the callback, the 'index' is checked. If there are more permissions to request,
// this method is called again. If not, the summary method below is called.
// It's ugly, but it is the only way I have been able to figure out how to place an explanation dialog before each
// Android System action for the permission. Using the multiple permission approach I could not get my dialogs to
// appear before each of the Android actions.
@SuppressLint("NewApi")
private void requestPermissions(int index)
{
if (reasonList.get(index).isEmpty()) // Work-a-round for Android 12. If explanation is empty then
{
// Skip explanation dialog but still request permission. Android pops up no dialog but auto-grants permission.
activityResultLauncher.launch(permissionsList.get(index));
return;
}
// Popup a dialog explaining why the app needs this permission and perhaps what Android is going to put you
// through to grant the permission. For example, for BLUETOOTH_CONNECT/SCAN permissions Android pops up a
// dialog. But for BACKGROUND permissions, Android presents an Activity. Exiting the dialog requires different
// behavior from the user than exiting an activity.
final androidx.appcompat.app.AlertDialog.Builder builder =
new androidx.appcompat.app.AlertDialog.Builder(new ContextThemeWrapper(this, R.style.Theme_AppCompat_Light));
builder.setTitle("Health@Home needs the following permission:");
if (isVersionN_Plus)
{
builder.setMessage(Html.fromHtml(reasonList.get(index), Html.FROM_HTML_MODE_LEGACY));
}
else
{
builder.setMessage(Html.fromHtml(reasonList.get(index)));
}
builder.setPositiveButton(android.R.string.ok, null);
builder.setOnDismissListener(dialog ->
{
Log.v(TAG, "HH2: Requesting permissions");
activityResultLauncher.launch(permissionsList.get(index));
});
builder.show();
}
// THis method just summarizes the results of the permissions.
@SuppressLint("NewApi")
private void handlePermissionSummary()
{
boolean connectOk = (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED);
boolean scanOk = (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED);
boolean locationOk = (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED);
boolean backgroundOk = (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED);
Log.d(TAG, "HH2: BLUETOOTH_CONNECT Permissions are " + connectOk);
Log.d(TAG, "HH2: BLUETOOTH_SCAN Permissions are " + scanOk);
Log.d(TAG, "HH2: ACCESS_FINE_LOCATION Permissions are " + locationOk);
Log.d(TAG, "HH2: ACCESS_BACKGROUND_LOCATION Permissions are " + backgroundOk);
String message = "";
if (!connectOk && isVersionS_Plus)
{
message = "<p><b>Bluetooth Connect permissions not given.</b> You will be unable to find and connect to Bluetooth Low Energy devices</p>";
}
if (!scanOk && isVersionS_Plus)
{
message = "<p><b>Bluetooth Scan permissions not given.</b> You will be unable to find and connect to Bluetooth Low Energy devices</p>";
}
if (!locationOk && !isVersionS_Plus)
{
message = "<p><b>Location permissions not given.</b> You will be unable to find and connect to Bluetooth Low Energy devices</p>";
}
if (!backgroundOk && isVersionO_Plus)
{
message = message + "<p><b>Background permissions not given.</b> Operations with Bluetooth Low Energy devices will only work " +
"while Health@Home PHG is visible on the screen.</p>";
}
if (!message.isEmpty())
{
message = message + "<p>Remedies:<br>1. Restart Health@Home PHG.<br>2. Set permissions directly in the Android Settings menu<br>" +
"3. Uninstall and re-install Health@Home PHG</p>";
final AlertDialog.Builder builder =
new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.Theme_AppCompat_Light));
builder.setTitle("Health@Home not given certain permissions!");
if (isVersionN_Plus)
{
builder.setMessage(Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY));
}
else
{
builder.setMessage(Html.fromHtml(message));
}
builder.setPositiveButton(android.R.string.ok, null);
builder.setOnDismissListener(dialog ->
{
Log.d(TAG, "Starting Login");
// setLoginScreen(); // Do whatever you would do after permissions are handled
});
builder.show();
return;
}
Log.d(TAG, "Starting Login");
// setLoginScreen(); // Do whatever you would do after permissions are handled
}
}
allow all the time
issue. – Monda