Baseflow / flutter-permission-plugins

This repo contains a collection of permission related Flutter plugins which can be used to request permissions to access device resources in a cross-platform way.
https://baseflow.com
MIT License
52 stars 34 forks source link

[android] "Can request only one set of permissions at a time." #53

Open daadu opened 3 years ago

daadu commented 3 years ago

🐛 Bug Report

Calling await LocationPermissions().requestPermissions(); with both "Fine" and "Coarse" permission in AndroidManifest.xml gives the Can request only one set of permissions at a time. in the logs. Also, giving PermissionStatus.denied (even if approved) when checkPermissions called after awaiting requestPermissions.

Expected behaviour

The error/warning should not come in and correct status should be returned by checkPermissions after requestPermissions is called.

Reproduction steps

Code to reproduce:


  Future<bool> _checkLocationPermission() async {
    final checkPermission = await LocationPermissions().checkPermissionStatus();
    printIfDebug("checkPermission: $checkPermission");
    if (checkPermission == PermissionStatus.granted ||
        checkPermission == PermissionStatus.restricted) return true;
    return false;
  }

  Future<bool> _checkAndRequestLocationPermission() async {
    // return true, if already have permission
    if (await _checkLocationPermission()) return true;

    // request permission
    final _ = await LocationPermissions().requestPermissions();
    printIfDebug("requestPermission: $_");

    // check if permission was given
    final hasPermission = await _checkLocationPermission();

    // if no permission and "showShowPermissionRationale" then go to settings and return false
    if (!hasPermission &&
        await LocationPermissions().shouldShowRequestPermissionRationale()) {
      // if shouldRequest false, then open app settings and return false
      // TODO UI that shows why this permission is required
      await LocationPermissions().openAppSettings();
      return false;
    }

    return hasPermission;
  }

This is how it it is used for checking access for location before calling WifiManager.startScan api on android

  Future<List<WifiNetwork>> scanWifi() async {
    if (await _checkAndRequestLocationPermission())
      return await WiFiForIoTPlugin.loadWifiList();
    return null;
  }

Configuration

Android SDK: 28

Version: ^3.0.0

Platform:

daadu commented 3 years ago

I am calling checkPermissions again after requestPermissions instead of "checking" the returned value of requestPermissions because it is returning Unkown status (I assume it is because the Activity is pausing-and-resuming after the permission is granted, I am using embedding v2).

daadu commented 3 years ago

Full logs:

I/flutter ( 4962): AppLifecycleState.inactive
I/flutter ( 4962): AppLifecycleState.inactive
I/flutter ( 4962): checkPermission: PermissionStatus.denied
...
W/Activity( 4962): Can request only one set of permissions at a time
...
I/flutter ( 4962): requestPermission: PermissionStatus.unknown
I/flutter ( 4962): checkPermission: PermissionStatus.denied
...
...
I/AppStateListener( 4962): onActivityStopped for activity : MainActivity isForegound : false
I/flutter ( 4962): AppLifecycleState.paused
I/flutter ( 4962): AppLifecycleState.paused
...
I/AppStateListener( 4962): onActivityStarted for activity : MainActivity isForegound : true
...
I/flutter ( 4962): AppLifecycleState.resumed
I/flutter ( 4962): AppLifecycleState.resumed
daadu commented 3 years ago

Also my guess the bug is in returning the result for requestPermissions.

daadu commented 3 years ago

The following code is working now.

uture<bool> _checkLocationPermission() async {
    final checkPermission = await LocationPermissions().checkPermissionStatus();
    printIfDebug("checkPermission: $checkPermission");
    if (checkPermission == PermissionStatus.granted ||
        checkPermission == PermissionStatus.restricted) return true;
    return false;
  }

  Future<bool> _checkAndRequestLocationPermission() async {
    // return true, if already have permission
    if (await _checkLocationPermission()) return true;

    // request permission
    PermissionStatus requestPermission = PermissionStatus.unknown;
    int count = 0;
    while (requestPermission == PermissionStatus.unknown && count < 20) {
      requestPermission = await LocationPermissions().requestPermissions();
      printIfDebug("requestPermission: $requestPermission");
      await Future.delayed(Duration(seconds: 1));
      count++;
    }

    // check if permission was given
    final hasPermission = await _checkLocationPermission();

    // if no permission and "showShowPermissionRationale" then go to settings and return false
    if (!hasPermission &&
        await LocationPermissions().shouldShowRequestPermissionRationale()) {
      // if shouldRequest false, then open app settings and return false
      // TODO UI that shows why this permission is required
      await LocationPermissions().openAppSettings();
      return false;
    }

    return hasPermission;
  }

Basically have added a while loop that keeps on requesting until it return unkown status.

The correct behaviour should be that the method returns proper status in one attempt without loop.

Could the bug be because of Acitivyt going inactive/pause when asking for permission causing detachment?

acarlstein commented 3 years ago

@daadu , I run into the same issue as you.

In my case, I created quickly a prototype of an activity as an experiment.

This activity would check each permission, ask the user to allow it, and if there was any permission that wasn't a allowed, then a dialog would show up. The dialog would explain the user why that permission was needed request again for that permission.

image

Note: I wouldn't be surprise that there is an error in the code below since I created in a rush Here is my initial code:

class PermissionsActivity : AppCompatActivity() {
    companion object {
        private val TAG = PermissionsActivity::class.java.simpleName
        private const val ACCESS_NETWORK_STATE_CODE : Int = 100
        private const val CHANGE_NETWORK_STATE_CODE : Int = 101
        private const val ACCESS_INTERNET_CODE : Int = 102
        private const val ACCESS_CAMERA_CODE : Int = 103
        private const val ACCESS_COARSE_LOCATION_CODE : Int = 104
        private const val ACCESS_FINE_LOCATION_CODE : Int = 105
        private const val ACCESS_BACKGROUND_LOCATION_CODE: Int = 106
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_permissions)
        requestPermissions()
        confirmPermissions()
    }

    private fun requestPermissions(){
        if (!hasPermission(Manifest.permission.ACCESS_NETWORK_STATE)) requestAccessNetwork()
        if (!hasPermission(Manifest.permission.CHANGE_NETWORK_STATE)) requestChangeNetwork()
        if (!hasPermission(Manifest.permission.CAMERA)) requestCamera()
        if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) requestFineLocation()
        if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
            && !hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) requestCoarseLocation()

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            if (!hasPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) requestBackgroundLocation()
        }
    }

    private fun hasPermission(id: String): Boolean {
        return ActivityCompat.checkSelfPermission(this, id) == PackageManager.PERMISSION_GRANTED
    }

    private fun requestAccessNetwork(){
        requestPermission(
            Manifest.permission.ACCESS_NETWORK_STATE,
            ACCESS_NETWORK_STATE_CODE
        )
    }

    private fun requestPermission(permission: String, code: Int){
        ActivityCompat.requestPermissions(
            this,
            arrayOf(permission),
            code
        )
    }

    private fun requestChangeNetwork(){
        requestPermission(
            Manifest.permission.CHANGE_NETWORK_STATE,
            CHANGE_NETWORK_STATE_CODE
        )
    }

    private fun requestCamera(){
        requestPermission(
            Manifest.permission.CAMERA,
            ACCESS_CAMERA_CODE
        )
    }

    private fun requestCoarseLocation(){
        requestPermission(
            Manifest.permission.ACCESS_COARSE_LOCATION,
            ACCESS_COARSE_LOCATION_CODE
        )
    }

    private fun requestFineLocation(){
        requestPermission(
            Manifest.permission.ACCESS_FINE_LOCATION,
            ACCESS_FINE_LOCATION_CODE
        )
    }

    @RequiresApi(Build.VERSION_CODES.Q)
    private fun requestBackgroundLocation(){
        requestPermission(
            Manifest.permission.ACCESS_BACKGROUND_LOCATION,
            ACCESS_BACKGROUND_LOCATION_CODE
        )
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String?>,
        grantResults: IntArray,
    ) {
        Log.d(TAG, "onRequestPermissionsResult()")
        when (requestCode) {
            ACCESS_NETWORK_STATE_CODE ->
                if (wasPermissionGranted(grantResults)) {
                    changeTintToGreen(R.id.imageViewNetworkAccess)
                } else {
                    showDialog(
                        "Access Network State Permission",
                        "Allow access network state permission to talk to the server",
                        requestAccessNetwork()
                    )
                }
            CHANGE_NETWORK_STATE_CODE ->
                if (wasPermissionGranted(grantResults)) {
                    changeTintToGreen(R.id.imageViewNetworkAccess)
                } else {
                    showDialog(
                        "Change Network State Permission",
                        "Allow change network state permission to talk to the server",
                        requestAccessNetwork()
                    )
                }
            ACCESS_INTERNET_CODE ->
                if (wasPermissionGranted(grantResults)) {
                    changeTintToGreen(R.id.imageViewInternetAccess)
                } else {
                    showDialog(
                        "Access Internet Permission",
                        "Allow access internet permission to talk to the server",
                        requestAccessNetwork()
                    )
                }
            ACCESS_CAMERA_CODE ->
                if (wasPermissionGranted(grantResults)) {
                    changeTintToGreen(R.id.imageViewCameraAccess)
                } else {
                    showDialog(
                        "Access Camera Permission",
                        "Allow access camera permission to scan QR barcodes",
                        requestAccessNetwork()
                    )
                }
            ACCESS_FINE_LOCATION_CODE ->
                if (wasPermissionGranted(grantResults)) {
                    changeTintToGreen(R.id.imageViewBackgroundLocationAccess)
                } else {
                    showDialog(
                        "Access Fine Location Permission",
                        "Allow access to fine location permission for fine location",
                        requestAccessNetwork()
                    )
                }
            ACCESS_COARSE_LOCATION_CODE ->
                if (wasPermissionGranted(grantResults)) {
                    changeTintToGreen(R.id.imageViewCoarseLocationAccess)
                } else {
                    showDialog(
                        "Access Coarse Location Permission",
                        "Allow access to coarse location permission for general location",
                        requestAccessNetwork()
                    )
                }
            ACCESS_BACKGROUND_LOCATION_CODE ->
                if (wasPermissionGranted(grantResults)) {
                    changeTintToGreen(R.id.imageViewNetworkAccess)
                } else {
                    showDialog(
                        "Access Background Location Permission",
                        "Allow access to background location permission for fine and general location in Android 10+",
                        requestAccessNetwork()
                    )
                }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

    private fun wasPermissionGranted(grantResults: IntArray): Boolean {
        return grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
    }

    private fun changeTintToGreen(id: Int){
        findViewById<ImageView>(id).setColorFilter(
            ContextCompat.getColor(
                this,
                R.color.green),
            android.graphics.PorterDuff.Mode.MULTIPLY
        );
    }

    private fun showDialog(title: String, message: String, callback: Unit){
        AlertDialog.Builder(this)
            .setTitle(title)
            .setMessage(message)
            .setPositiveButton("Allow") { _, _ ->
                callback
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
                setResult(RESULT_CANCELED, Intent())
                finish()
            }
            .create()
            .show()
    }

    private fun confirmPermissions() {
        var result = true
        result = result and hasPermission(Manifest.permission.ACCESS_NETWORK_STATE)
        result = result and hasPermission(Manifest.permission.CHANGE_NETWORK_STATE)
        result = result and hasPermission(Manifest.permission.CAMERA)
        result = result and hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
        if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
            result = result and hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            result = result and hasPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
        }
        if (result){
            setResult(RESULT_OK, Intent())
        } else {
            setResult(RESULT_CANCELED, Intent())
        }
        finish()
    }
}

Personally, it would be great that the Android API would take care of asking the user for the permissions automatically based on what is in the AndroidManifest.xml without the developer having to spend time implementing it. It would safe lots of time but I guess that will never be the case.