'When User rejects background location permissions using ActivityResultLauncher,, the activity is restarted
I am asking for a set of permissions on Android OS 6+ for the use of Bluetooth. Pre-12 the run time permissions required are ACCESS_FINE_LOCATION (for use of the Bluetooth Low Energy Scanner) and ACCESS_BACKGROUND_LOCATION to use the same scanner in a service running in the background.
In the Android 11 case that is 2 permissions and two 'reason' dialogs. So if a permission is needed, I popup the reason dialog, and when the user clicks okay, I call the ActivityResultLauncher with the requested permission. Android pops up the appropriate dialog and when the user makes the decision, the ActivityResultLauncherCallback is signaled and I get the result. If I have another permission, I repeat the process until done.
This works fine for the ACCESS_FINE_LOCATION whether the user grants or rejects the permission. For or the ACCESS_BACKGROUND_LOCATION permission it works fine if the user grants the permission, but if the user rejects the permission, the ActivityResultLauncherCallback is not signaled. Instead, the activity is restarted. On the restart, Bundle saveInstanceState is non-null.
Here is the code for the entire activity (which is just for handling runtime permissions). It has a lot of explanatory text - I am assuming it will help some Bluetooth programmers but I need it to work first!
On an Android 12 device, the sequence works and the deny does not cause a problem. The problem is replicated on a Pixel 2 Android 11.
package com.pcha.phg1;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.text.Html;
import android.util.Log;
import androidx.activity.result.ActivityResult;
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;
/**
* This is now the start activity to deal with the new
* permissions that must be confirmed by the application user for versions of Android 6 and up.
* Android 8 and up introduces further complexities in the overlay windows (Alert Dialogs).
* Additional permissions are required and the name of the window type changes from
* TYPE_SYSTEM_ALERT to TYPE_APPLICATION_OVERLAY which requires another if-then-else in any
* popup dialog methods.
*/
@SuppressLint("NewApi")
public class PermissionsActivity extends AppCompatActivity
{
private static final String TAG = "PermissionsActivity";
private static final int SPLASH_DISPLAY_LENGTH = 2000;
Context context = null;
// Catch bad things. There will always be bad things - no one is perfect.
private final Thread.UncaughtExceptionHandler androidDefaultUEH = Thread.getDefaultUncaughtExceptionHandler();
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 isVersionQ_Plus = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q);
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 isVersionQtoR = ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) // 29
&& (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 = 0; // The maximum number of permissions being asked for (set below)
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.splash);
context = this;
// Set up our 'bad things will happen' exception handler
Thread.setDefaultUncaughtExceptionHandler(new AppDefaultExceptionHandler());
/* New Handler to start the main Activity
* and close this Splash-Screen after some seconds.
* and handle permissions should they be needed */
if(savedInstanceState == null) // If null this is application start up. Non-null on a screen rotation which we currently prevent.
{
// This post-delayed handler just allows the display of some splash screen for SPLASH_DISPLAY_LENGTH
new Handler().postDelayed(() ->
{
if (isVersionM_Plus) // Permissions needed for Andoid OS >= 6
{
// Do we have Overlay permissions for our popup discovery dialogs?
if (!Settings.canDrawOverlays(context))
{
// Create a dialog displaying to user why we need this permission
final AlertDialog.Builder builder =
new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.Theme_AppCompat_Light));
builder.setTitle("Overlay Permissions Needed!");
String message = getString(isVersionR_Plus ?
R.string.permissions_overlay_11 :
R.string.permissions_overlay);
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.v(TAG, "User read Overlay info");
// Call method to invoke Android Settings Activity for user to grant/reject such permission
getSystemWindowAlertPermission();
});
builder.show();
}
// We have overlay permissions.
else
{
// Method to check for and get the remaining permissions
handlePermissions();
}
}
// Pre-Android 6 we don't need any permissions - Start the main activity immediately
else
{
Intent start = new Intent(context, Phg1_Activity.class);
activityResultExitLauncher.launch(start);
}
}, SPLASH_DISPLAY_LENGTH);
}
}
// We will return here when the main activity finishes
ActivityResultLauncher<Intent> activityResultExitLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>()
{
@Override
public void onActivityResult(ActivityResult result)
{
finish();
}
});
// We will return here when the user responds to the Android System Dialog for granting/rejecting permissions
ActivityResultLauncher<String> activityResultLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback<Boolean>()
{
@Override
public void onActivityResult(Boolean result)
{
// Log what the user did
Log.d(TAG, "HH2: Permission " + permissionsList.get(index) + (result ? " granted" : " rejected"));
// Increment the permission we are now handling
index++;
// Have we handled all the permissions?
if (index >= indexMax)
{
// If so, this method checks to see if any of the requested permissions were rejected
// and pops up a dialog giving the consequences of that rejection before starting the
// main activity. Otherwise the main activity is started directly
handlePermissionSummary();
}
else
{
// Otherwise, Handle the next permission
requestPermissions(index);
}
}
});
// Method checks to see if the needed permissions are granted and if they are the permissions
// summary is called, otherwise the permission granting/rejection process is started
@SuppressLint("NewApi")
private void handlePermissions()
{
// First make a list of the permissions we need and along with that list
// make a list of 'reasons' explaining to the user why that permission is needed
// The permissions needed will depend upon the Android version
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));
indexMax++;
}
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));
indexMax++;
}
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++;
}
}
else if (isVersionM_Plus) // Android 6+
{
// 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, 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++;
}
if (isVersionQtoR) // Android 10 - 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++;
}
}
}
// If no permissions are needed, handle the permission summary
if (permissionsList.size() == 0)
{
// We need to call the permissions summary because the overlay permission may not have been granted
// We know that all the other permissions have been granted.
// If the overlay permission has not been granted, we want to display the consequences to the user before
// continuing to the main activity.
handlePermissionSummary();
return;
}
// Otherwise, begin the permission request sequence which involves launching permission requests.
// The process is asynchronous, so the launch returns immediately. The permission sequence is
// Display reason dialog, and then call the Android OS handler that pops up the dialog
// When the user handles the dialog, that permission is done and the next permission in the list is handled
// When all done, call the permission summary handler.
requestPermissions(index);
}
// 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 AlertDialog.Builder builder =
new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.Theme_AppCompat_Light));
builder.setTitle("PchaDemoPhg 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 ->
{
// The user has read our explanation and we know launch the
// sequence that tells the Android OS to give the user the permission
// grant/reject dialog. Response is found in the activityResultLauncher callback
Log.v(TAG, "HH2: Requesting permissions");
activityResultLauncher.launch(permissionsList.get(index));
});
builder.show();
}
// THis method just summarizes the results of the permissions.
// If all the permissions have been granted, the main activity is started
// IF some are not, the consequences of the rejects are displayed to the
// user in a dialog before the main activity is started
@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);
boolean overlaysOk = Settings.canDrawOverlays(context);
// Here is the list of all the permissions we ask for:
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);
Log.d(TAG, "HH2: CAN DRAW OVERLAYS Permissions are " + overlaysOk);
// Create the message for the dialog
String message = "";
if (!connectOk && isVersionS_Plus)
{
message = getString(R.string.no_permission_connect_12);
}
if (!scanOk && isVersionS_Plus)
{
message = getString(R.string.no_permission_scan_12);
}
if (!locationOk && !isVersionS_Plus)
{
message = getString(R.string.no_permission_location);
}
if (!backgroundOk && isVersionQ_Plus)
{
message = message + getString(R.string.no_permission_background);
}
if (!overlaysOk && isVersionM_Plus)
{
message = message + getString(R.string.no_permission_overlay);
}
// If one or more permissions are not granted, pop up a 'consequences' dialog
if (!message.isEmpty())
{
message = message + getString(R.string.no_permissions_remedies);
final AlertDialog.Builder builder =
new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.Theme_AppCompat_Light));
builder.setTitle(getString(R.string.app_name) + " 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 " + getString(R.string.app_name));
Intent start = new Intent(context, Phg1_Activity.class);
activityResultExitLauncher.launch(start);
});
builder.show();
return;
}
// Otherwise, start the main activity
Log.d(TAG, "Starting " + getString(R.string.app_name));
Intent start = new Intent(context, Phg1_Activity.class);
activityResultExitLauncher.launch(start);
}
ActivityResultLauncher<Intent> activityResultOverlayLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>()
{
@Override
public void onActivityResult(ActivityResult result)
{
// Don't check for window overlays here - might be too soon and even though permissions are granted its not in the settings yet
// Now check for the remaining permissions. If this permission is rejected, the permission summary
// will display the consequences of the rejection.
handlePermissions();
}
});
@TargetApi(Build.VERSION_CODES.M)
private void getSystemWindowAlertPermission()
{
Log.d(TAG, "Checking for permission to allow Discovery dialog popups");
if (isVersionM_Plus && !Settings.canDrawOverlays(this))
{
final Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
try
{
activityResultOverlayLauncher.launch(intent);
}
catch (ActivityNotFoundException e)
{
Log.e(TAG, e.getMessage());
}
}
}
// Lets trap uncaught exceptions ... no one is perfect. Here one can log
// and kill the app to assure the scanner doesnt continue to run...
class AppDefaultExceptionHandler implements Thread.UncaughtExceptionHandler
{
private Thread t;
private Throwable e;
AppDefaultExceptionHandler()
{
}
@Override
public void uncaughtException(Thread t, Throwable e)
{
this.t = t;
this.e = e;
// Save stuff here
Log.e(TAG, "Uncaught exception thrown in " + t.getName() +
". Exception error message: " + e.getMessage());
e.printStackTrace();
callDefaultExceptionHandler();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}
void callDefaultExceptionHandler()
{
if (androidDefaultUEH != null) androidDefaultUEH.uncaughtException(t, e);
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
