Because the Android SDK 23 gives users the possibility to deny apps access to certain functionalities I wanted to update one of my apps to request permissions as it is described in here: https://developer.android.com/preview/features/runtime-permissions.html.
In one of the activities I embed a SupportMapFragment. To make it work you need to have the WRITE_EXTERNAL_STORAGE permission, so I request it when I start the activity which results in a creation of a permission request dialog.
Now the problem is that when the dialog is still open and I rotate the device the activity will be restarted and open a new permission request dialog while the old one is still there. The result is two of those dialogs on top of each other and only one of it being useful.
Is there a way to get rid of the dialog that was started first?
As CommonsWare said in his comment the best solution is to put a boolean into the savedInstanceState-Bundle to know if the dialog is still open.
Example:
// true if dialog already open
private boolean alreadyAskedForStoragePermission = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null) {
alreadyAskedForStoragePermission = savedInstanceState.getBoolean(STORAGE_PERMISSION_DIALOG_OPEN_KEY, false);
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY, alreadyAskedForStoragePermission);
}
private void checkStoragePermission(){
if(alreadyAskedForStoragePermission){
// don't check again because the dialog is still open
return;
}
if(ActivityCompat.checkSelfPermission(this, STORAGE_PERMISSIONS[0]) != PackageManager.PERMISSION_GRANTED){
// the dialog will be opened so we have to keep that in memory
alreadyAskedForStoragePermission = true;
ActivityCompat.requestPermissions(this, STORAGE_PERMISSIONS, STORAGE_PERMISSION_REQUEST_CODE);
} else {
onStoragePermissionGranted();
}
}
#Override
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
switch (requestCode){
case STORAGE_PERMISSION_REQUEST_CODE:
// the request returned a result so the dialog is closed
alreadyAskedForStoragePermission = false;
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
onStoragePermissionGranted();
}
break;
}
}
As #user1991776 mentioned there is actually an undocumented extra that contains whether or not there is a permission dialog open at the moment, in Activity:
private static final String HAS_CURENT_PERMISSIONS_REQUEST_KEY =
"android:hasCurrentPermissionsRequest";
However there is a better way. When you request a permission dialog the second time (due to a rotation), Activity automatically cancels the old dialog by calling your onRequestPermissionResult() with empty arrays:
public final void requestPermissions(#NonNull String[] permissions, int requestCode) {
if (mHasCurrentPermissionsRequest) {
Log.w(TAG, "Can reqeust only one set of permissions at a time");
// Dispatch the callback with empty arrays which means a cancellation.
onRequestPermissionsResult(requestCode, new String[0], new int[0]);
return;
}
Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
mHasCurrentPermissionsRequest = true;
}
Or course this behaviour isn't documented because this is Android, and who wants to document complex behaviour?
Anyway you can just always request permissions in onCreate() and then ignore calls to onRequestPermissionsResult() with zero-length permissions arrays.
I guess as this is a system dialog you cannot control it. You could instead prevent that your activity gets reloaded if you turn your device.
Related
Background
I've noticed that ever since Android O came, the function of Settings.canDrawOverlays had issues telling us if the app was granted with the draw-on-top permission (AKA "Display over other apps"), so I've reported about this:
https://issuetracker.google.com/issues/62047810
https://issuetracker.google.com/issues/68465333
The problem
Not sure why they were marked as fixed, but now that I've tested on my Pixel 2 with Android 8.1, I've noticed this function still ALWAYS returns false (so I've reported about this here) .
Thing is, I've tried finding an alternative and saw others also having this issue here on StackOverflow:
Settings.canDrawOverlays is returning false even after turning the permission on from settings
Settings.canDrawOverlays(context) returns false on Android Oreo
Why in Android O method Settings.canDrawOverlays() returns "false" when user has granted permission to draw overlays and returns to my application?
I don't know if this is an issue with Android 8.1, or just my Pixel 2, but I'm using the latest, stock rom version of it (OPM171019.013) .
What I've tried
I've tried all of the solutions that were suggested on the above links.
All of them return me the result that the permission is not granted, ever.
The only solution I've found that does something is this one, which just tells me that the permission was toggled.
One way to "solve" this, is to assume the permission is granted in case the app was installed via the Play Store, because it's granted by default when installing from there.
But this is not a good solution. Apps can be installed from outside of the Play Store, and the user can always turn off this permission manually.
EDIT: I don't know if that's even a possible "solution", because when I try on the app, I can't really draw on top, even if the permission is granted.
The questions
How can I check for sure if the current app is granted with this permission?
From which Android version does this issue occur? Or maybe it's an issue only for Pixel 2 ?
Seeing that even after granting the permission, the app fails to draw on top, is there a possible solution for it?
EDIT: Speaking with others, I think this is a very specific issue on Pixel 2 with Android 8.1 . The reason is that the issue doesn't seem to appear on other devices with Android 8.1. I hope it gets fixed.
I had the same issue, there is a problem with the function Settings.canDrawOverlays because that needs an app reboot to return true when you granted the permission, try these.
I have a button that calls a Config Activity to enable Overlay
public void setOverlayPermissions(){
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:"+getPackageName()));
startActivityForResult(intent,app.REQUEST_ID_OVERLAY_PERMISSIONS);
}
After that, in onActivityResult funtion
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == app.REQUEST_ID_OVERLAY_PERMISSIONS){
new WaitingFor().execute();
}
}
And then
private class WaitingFor extends AsyncTask{
private ProgressDialog progressDialog;
#Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setCancelable(false);
progressDialog.setMessage("Espera un momento...");
progressDialog.show();
}
#Override
protected Object doInBackground(Object[] objects) {
try{
Thread.sleep(2000);
}catch (Exception ex){
Log.e(TAG,ex.getMessage()+"");
}
return null;
}
#Override
protected void onPostExecute(Object o) {
super.onPostExecute(o);
progressDialog.dismiss();
if(verifyWindowOverlay()){
FL.w(TAG,"Acepto Overlay");
setPaso2();
}else{
FL.w(TAG,"NO acepto Overlay [Permission error]");
Toast.makeText(MainActivity.this,"Falta configuracion, reintenta!", Toast.LENGTH_SHORT).show();
}
}
}
My verifyWindowsOverlay function
public boolean verifyWindowOverlay(){
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
return Settings.canDrawOverlays(this);
if(appOpsMgr.checkOpNoThrow(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, android.os.Process.myUid(), getPackageName()) == 0 || Settings.canDrawOverlays(this))
return true;
return false;
}
Good luck.
I have a VideoCallPageRenderer like below
VideoCallPageRenderer : PageRenderer, Android.Support.V4.App.ActivityCompat.IOnRequestPermissionsResultCallback
{
public const int REQUEST_MIC = 0;
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
// Other codes
RequestMicPermission();
}
public void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults)
{
if (requestCode == REQUEST_MIC)
{
if (grantResults.Length == 1 && grantResults[0] == Permission.Granted)
{
}
}
}
private void RequestMicPermission()
{
Android.Support.V4.App.ActivityCompat.RequestPermissions((Activity)Xamarin.Forms.Forms.Context, new string[] { Android.Manifest.Permission.RecordAudio }, REQUEST_MIC);
}
}
Here RequestMicPermission works fine because I can see the pop up coming on screen asking for permission. But after I allow or deny OnRequestPermissionsResult is not called.
Any help? It would be very hard to try to override it in the Activity.
For anyone else having an issue here, I was not seeing the OnRequestPermissionsResult being called either. But I had the debugger attached, the handler wasn't doing anything, and even though the breakpoint was valid, it was never hit.
I finally did a clean and rebuild and voila, the breakpoint was hit.
Just in case someone else was tearing their hair out like myself :).
For future readers that encountered this problem too, a good approach is abstract the permissions features in a service and register it with Xamarin Form´s Dependencyservice.
You can resolve this service in your renderer, or any non-activity class, to use it. Also resolve it in MainActivity, override OnRequestPermissionsResult, and call yourService.OnPermissionResult to communicate the result.
As Koushik says ActivityCompat.IOnRequestPermissionsResultCallback doesnt work because we call RequestPermissions with reference to MainActivity, so the result is obtained in this activity.
You can see an implementation example in my Github:
GpsService
MainActivity
I am new to android. I am trying to grant permissions from custom class. I have two interfaces and a singleton class, like this
public interface RequestPermissionsResultInterface
{
void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults);
}
public interface PermissionManagerInterface
{
void onPermissionGranted(String message, int requestCode);
void onPermissionDenied(String message, int requestCode);
}
public class PermissionManager
{
private Activity mActivity;
private static volatile PermissionManagerInterface mManagerInterface;
public static PermissionManager getInstance(Context context)
{
if (mPermissionManager == null)
{
synchronized (PermissionManager.class)
{
if (mPermissionManager == null)
{
mPermissionManager = new PermissionManager(context);
}
}
}
return mPermissionManager;
}
private boolean isReadStorageAllowed()
{
int result = ContextCompat.checkSelfPermission(this.mActivity, Manifest.permission.READ_EXTERNAL_STORAGE);
return ((result == PackageManager.PERMISSION_GRANTED));
}
public RequestPermissionsResultInterface askPermission(
Activity mActivity,
String permissionName,
final PermissionManagerInterface managerInterface,
final int requestCode)
{
boolean isReadExternalStorageAllowed = isReadStorageAllowed();
if(isReadExternalStorageAllowed == false)
{
if (ActivityCompat.shouldShowRequestPermissionRationale(mActivity, permissionName))
{
final AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setMessage("Please allow all permissions in App Settings for additional functionality.");
builder.setCancelable(false);
builder.setPositiveButton("Allow", new DialogInterface.OnClickListener() {
public void onClick(#SuppressWarnings("unused") final DialogInterface dialog, #SuppressWarnings("unused") final int id) {
managerInterface.onPermissionGranted("Permission Granted", requestCode);
}
});
builder.setNegativeButton("Deny", new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, #SuppressWarnings("unused") final int id) {
managerInterface.onPermissionDenied("Permission Denied", requestCode);
}
});
final AlertDialog alert = builder.create();
alert.show();
}
}
else managerInterface.onPermissionGranted("Permission Already Granted", requestCode);
}
}
And here is how I use this class in MainActivity
PermissionManager permManager = PermissionManager.getInstance();
permManager.askPermission(
MainActivity.this,
Manifest.permission.READ_EXTERNAL_STORAGE,
mPermissionManagerInterface, // assume this exists
EXTERNAL_STORAGE_PERMISSION_CODE);
Again.... it is not persistent. If I give permission, then stop and restart the app again....this is asking me for permission again. What could be wrong?? Thanks
If you are determined to go the "custom" method, you still need to handle certain things.
shouldShowRequestPermissionRationale is only true if you have previously requested a permission and was denied it. You seem to be using it to display a dialog saying "please grant permissions" but not actually asking the system to show the permission request dialog.
The command to actually request permissions is ActivityCompat.requestPermissions. Can't see this in your code. The user cannot grant permission unless you actually ask for them.
onRequestPermissionsResult is where you should be calling your interface to say permission was granted.
Full instructions are in the documentation.
https://developer.android.com/training/permissions/requesting.html
From Android Documentation
Note: When your app calls requestPermissions(), the system shows a standard dialog box to the user. Your app cannot configure or alter that dialog box.
I was trying to grant permission through custom dialog which was never going to work. This was only part of the problem.
I was not requesting permission in custom dialog code section (which would have been bad because I end up showing 2 dialogs: standard + my custom dialog). So I remove my custom dialog and stick to the standard one.
Kudos goes to kuffs because s/he put me in the right direction.
I'm developing a custom compound View that needs to access external storage. How can I implement the permission handling without involving outside parties, i.e. Activity or Fragment?
I get that I can request the permissions using the View's context, but how can I handle onRequestPermissionsResult() inside the View? Is it even possible?
If it's not possible, what would be the most elegant solution to handle something like this?
I'm developing a custom compound View that needs to access external storage
IMHO, that's an architecture bug. A View is for displaying stuff to the user, and sometimes for collecting low-level input events and turning them into higher-order constructs (e.g., clicks, swipes). A View should not have any connection to files, databases, etc. See the MVC, MVP, MVVM, and similar GUI architecture patterns.
WebView, which does not abide by this, causes problems (e.g., doing disk I/O on the main application thread) as a result.
How can I implement the permission handling without involving outside parties, i.e. Activity or Fragment?
You can't. It is the responsibility of the activity or fragment to request the permission, presumably before your view needs this data.
what would be the most elegant solution to handle something like this?
Extract the data-access portion of this View into something else that is managed by the activity or fragment, where the threading, permissions, and other work associated with that data access can be managed.
You can't work with permissions without the instance of the activity, but you can do your code prettier. If you want to send a request and handle it in one place, then you can use the example below.
Just create something looks like BaseActivity and put there such code
public class PermActivity extends Activity {
interface OnPermissionCallback{
void requestResult(String[] permissions, int[] grantResults);
}
private SparseArray<OnPermissionCallback> permissionCallback = new SparseArray<>();
#Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
permissionCallback.get(requestCode).requestResult(permissions, grantResults);
}
public void addPermissionCallback(int requestCode, OnPermissionCallback callback){
permissionCallback.put(requestCode, callback);
}
}
And now in our client code, we can do something like that
class SomeClasThatWorksWithPerms{
private PermActivity activity;
public SomeClasWorksWithPerms(PermActivity activity) {
this.activity = activity;
}
void foo(){
if (ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
// do something
}else {
activity.addPermissionCallback(0, (perms, grantResults) -> {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
foo(); // try one more
}
});
activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 0);
}
}
}
I have used spareArray and indexation by the request code but you can use another way of storing callbacks.
It's very simple example, you can see something more serious there
https://github.com/mrizyver/Fl_Launcher/blob/master/app/src/main/java/com/izyver/fllauncher/presentation/activity/FlActivity.kt - as you can see, it is activity
https://github.com/mrizyver/Fl_Launcher/blob/master/app/src/main/java/com/izyver/fllauncher/presentation/loaders/WallpaperLoader.kt - our client code that works with permissions
let us assume you need to call the requestPermissionLauncher from a dialog fragment when a user clicks on "OK" or some other button. here is the requestPermissionLauncher found in MainActivity or you can put it in any other activity where the dialog fragment is called from.
public ActivityResultLauncher<String> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
// Permission is granted. Continue the action or workflow in your
// app.
} else {
// Explain to the user that the feature is unavailable because the
// features requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
}
});
here is the code source if you want to refer https://developer.android.com/training/permissions/requesting
Then in your dialog fragment use the following code to call to the instance requestPermissionLauncher
((MainActivity)getContext()).requestPermissionLauncher.launch(Manifest.permission.[*your permission goes here*]);
It's only possible in Activities and Fragments.
What you can do is copy public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) in your View and call that method in the corresponding one in the Activity or Fragment where the Context is.
I recently integrated Google's Smart Lock for Passwords feature into my app and almost everything is running smoothly as expected.
There is just one small issue I was not able to fix yet: In ResultCallback#onResult, if status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED the following command leads to the presentation of a Google resolution dialog that is asking whether to save the credentials via Smart Lock (see attached image) or which credentials to use, if there are already multiple credentials saved in Smart Lock:
status.startResolutionForResult(getActivity(), REQUEST_CODE_READ);
When the resolution dialog is presented, and the user does some orientation changes, then the resolution dialog multiplies, each of them overlapping the others. As a user, you first don’t see that there are multiple copies of the dialog, but if you close the first (by tapping on „Never“ or „Save Password“) then the uppermost dialog disappears, revealing another identical dialog below.
You can handle this by maintaining some state between the activity starting and stopping.
See use of the mIsResolving variable in this sample code. Simply save whether there is a pending dialog already when onSaveInstanceState() is called and restore in onCreate(), and guard against calling the API again if so, clearing the state once onActivityResult() is received for the intent.
private void resolveResult(Status status, int requestCode) {
// We don't want to fire multiple resolutions at once since that can result
// in stacked dialogs after rotation or another similar event.
if (mIsResolving) {
Log.w(TAG, "resolveResult: already resolving.");
return;
}
if (status.hasResolution()) {
try {
status.startResolutionForResult(MainActivity.this, requestCode);
mIsResolving = true;
...
#Override
protected void onCreate(Bundle savedInstanceState) {
...
if (savedInstanceState != null) {
mIsResolving = savedInstanceState.getBoolean(KEY_IS_RESOLVING);
}
...
#Override
protected void onSaveInstanceState(Bundle outState) {
...
outState.putBoolean(KEY_IS_RESOLVING, mIsResolving);
...
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
...
mIsResolving = false;
...
This is a common pitfall for many apps, so we'll look into whether we can support this state in Play Services layer, but for now, using the boolean for the activity is the current and general recommendation for maintaining resolution state.
I know, that it's an old question, but recently i have to fight with this issue, in my case I was using status.startResolutionForResult() in custom class and i didn't have any access to onSaveInstanceState() (I could make some custom callback with interface, but i didn't want to), but in my custom class i had an instance of an activity, so always before calling startResolutionForResult() I'm checking mActivity.hasWindowFocus() to see if activity lose focus, becouse of dialog that show, if it's true, then I call startResolutionForResult(), otherwise i do nothing
#Override
public void onResult(#NonNull LocationSettingsResult result) {
final Status status = result.getStatus();
switch (status.getStatusCode()){
case LocationSettingsStatusCodes.SUCCESS:
getLocation();
break;
case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
if (mActivity.hasWindowFocus()) {
try {
status.startResolutionForResult(mActivity, SETTINGS_CHECK);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
break;
case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
mReceiver.unableToObtainLocation();
break;
}
}