Flutter-geolocator: Android permission "ask every time" reported as deniedForever

Created on 11 Feb 2021  路  7Comments  路  Source: Baseflow/flutter-geolocator

馃悰 Bug Report

On Android 11, tested on a Pixel 4a, when the permissions have been set to "ask every time" the checkPermission method returns LocationPermission.deniedForever.

Expected behavior

Unsure if what the right behaviour is. We have not been denied forever just we need to ask each time like we would if this was the first time. Might need something like LocationPermission.needToAsk etc.

Reproduction steps

Go to an App's settings in the Settings app.
Change the permission for Location to "ask every time".

image

Run code:

    LocationPermission permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.deniedForever) {
      print('denied forever');
    }

Configuration

Version: 6.2.0

Flutter version: 1.22.6

Platform:

  • [ ] :iphone: iOS
  • [X] :robot: Android
android bug

Most helpful comment

That was an amazingly detailed answer 馃憦

Imagine a world where this API was simple and provided the information required in a simple way -looks with dagger eyes at Android- 馃槕 .

I thank you for all the hard work and effort going into this. It is a hard problem and these sort of things make it unnecessarily worse. 馃槃

All 7 comments

After a bit of reading it feels like denied should be the answer rather than deniedForever. Will do some more digging but in the meantime if this is a design decision it would be great to here about the rationale etc :+1:

EDIT: tried a few things and am struggling to reproduce now :/ Wonder if the shared preferences aspect has some weird combination of events where it gets "stuck" in to thinking its forever denied.

@grahamsmith you are right, this is a bug which I am solving at the moment. The problem is indeed caused by storing that permission is denied forever in the shared preferences.

Pre API 30, people have been asking for a way to detect on Android when a user selected the Don't ask me again checkbox when denying permissions. This way developers can request users to go to the settings and update the permissions. Unfortunately Android doesn't have an API to request if the user checked the box yes or no. What they did have is the shouldShowRequestPermissionRationale method (official documentation can be found here), which after requesting permission will return true if the user denied the permissions without checking the "Don't ask me again" checkbox and false if the user did check the box. Meaning pre-API 30 this was a reliable way to determine if permissions were denied forever or not.

Since the shouldShowRequestPermissionRationale would also return false before you request permission the first time you start your App, we store a setting in the shared preferences indicating if the last time we requested permissions they were denied forever. Using this we could use the following decisions in the checkPermission method when Android returns that permissions are denied:

  1. If the App never requested permissions before and the shouldShowRequestPermissionRationale returns false, we can safely return LocationPermission.denied;
  2. If the last time the App requested permission they were denied forever but the shouldShowRequestPermissionRationale returns true, we can return LocationPermission.denied and the App is allowed to request permissions again;
  3. If the last time the App requested permission they were denied and the shouldShowRequestPermissionRationale returns false, we can return LocationPermission.deniedForever and the developer knows the App cannot request permissions and should redirect the user to the settings;

Of course if Android's permission system returns that permissions are granted the above doesn't apply since we have permissions and we can go ahead requesting a location.

With API 30 of Android, Google introduced the new Ask every time permissions, which basically resets the permissions each time the App is closed. This means that when starting the App and checking permissions Android will return the default denied permission (as Android implicitly denies permissions until the user explicitly granted them). Also the shouldShowRequestPermissionRationale will again return false. The problem occurs when the user chooses the "Ask every time" permission in the settings after denying permissions forever earlier. The geolocator stored this decision into the shared preferences, which means when the App now starts and checks permissions the code will run into the following:

  1. Android returns permissions are denied;
  2. The shouldShowRequestPermissionRationale returns false;
  3. The values stored in shared preferences indicate that last time the permissions were denied forever;

Resulting in the geolocator thinking the permissions should still be deniedForever. Before API 30 this was not a problem because the "Ask every time" permission didn't existed and the user would simply grant permissions in the settings.

To solve this we need to ditch storing the deniedForever result in the shared preferences, which will mean that the checkPermission will no longer be able report permissions are deniedForever (Android still doesn't provide any API to request this information).

I have updated the code in such a way that the requestPermission will be able to return permission has been denied forever after the user denied it for a second time, but this will only be kept in memory as long as the App is running. When
the user denied permissions forever, kills the App and starts it again and you call checkPermission it will return that permissions are denied. If you now request permission it wil not show a permission dialog and immediately return that permissions are denied forever.

I hope that you are able to follow this, please let me know if things are not entirely clear and I would be more than happy to explain. Also if you have any other idea's I would be very much open for them.

As mentioned I am working on the changes above and currently testing them. I am planning to release this soon as version 7.0.0 (which will also include null safety support).

That was an amazingly detailed answer 馃憦

Imagine a world where this API was simple and provided the information required in a simple way -looks with dagger eyes at Android- 馃槕 .

I thank you for all the hard work and effort going into this. It is a hard problem and these sort of things make it unnecessarily worse. 馃槃

Hi @grahamsmith,

I have just released version 7.0.0-nullsafety.7 of the geolocator plugin which addresses this issue. Unfortunately due to the fact this is a breaking change and we already had nullsafety support lined up, this means you need to also migrate to null safety to use this version in your App.

I did do another write up of the bug and how it is solved, if you are interested you can find the documentation here: https://github.com/Baseflow/flutter-geolocator/wiki/Breaking-changes-in-7.0.0#android-permission-update.

I will be closing this issue for now, please let me know if you have any feedback (and if required I would gladly reopen the issue).

Hi, i am using version 7.0.1 and i keep getting deniedForever, without ask dialog, my divice is Samsung galaxy s10e, Android 11

Pixel 4xl the same

@ron-diesel that is probably because permissions have been denied forever earlier. In this case you will need to go into the Android settings and change the permissions there. See also the comment above for more detailed explanation of the behaviour, specifically the section:

Resulting in the geolocator thinking the permissions should still be deniedForever. Before API 30 this was not a problem because the "Ask every time" permission didn't existed and the user would simply grant permissions in the settings.

To solve this we need to ditch storing the deniedForever result in the shared preferences, which will mean that the checkPermission will no longer be able report permissions are deniedForever (Android still doesn't provide any API to request this information).

I have updated the code in such a way that the requestPermission will be able to return permission has been denied forever after the user denied it for a second time, but this will only be kept in memory as long as the App is running. When
the user denied permissions forever, kills the App and starts it again and you call checkPermission it will return that permissions are denied. If you now request permission it wil not show a permission dialog and immediately return that permissions are denied forever.

You can create a simple testing App you can use to verify this behaviour:

  1. Create a new App with: flutter create geo_perm_test;
  2. Open the new App in a code editor (I used Visual Studio Code but anything would do fine);
  3. Add a dependency to the geolocator in your pubspec.yaml:
dependencies:
  flutter:
    sdk: flutter

  geolocator: ^7.0.1
  1. Add permissions to your android/app/src/main/AndroidManifest.xml file:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.geo_perm_test">

   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

   ...
</manifest>
  1. Replace the contents of the lib/main.dart with the following test code:
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, this.title}) : super(key: key);

  final String? title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  LocationPermission _permission = LocationPermission.denied;

  Future<void> _requestPermission() async {
    final permission = await Geolocator.requestPermission();

    setState(() {
      _permission = permission;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Current permission is:',
            ),
            Text(
              '$_permission',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _requestPermission,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
  1. Run the App and hit the floating action button to request permissions.
Was this page helpful?
0 / 5 - 0 ratings