Skip to content

Write permission not granted when requestion storage access. #642

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
1 of 2 tasks
0x-cell opened this issue Jul 29, 2021 · 9 comments
Closed
1 of 2 tasks

Write permission not granted when requestion storage access. #642

0x-cell opened this issue Jul 29, 2021 · 9 comments

Comments

@0x-cell
Copy link

0x-cell commented Jul 29, 2021

🐛 Bug Report

When accepting permissions for storage access and then trying to create a folder in Documents directory I'm getting following error:

I/flutter (14558): FileSystemException: Creation failed, path = '/storage/emulated/0/Documents/Test' (OS Error: Permission denied, errno = 13)

I'm using following request which I'm expecting both read & write permissions would be granted:

 if (!await Permission.storage.request().isGranted) {

As it turns out, it only grants read permissions:

runtime permissions:
android.permission.READ_EXTERNAL_STORAGE: granted=true, flags=[ USER_SET|USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHEN_DENIED|RESTRICTION_INSTALLER_EXEMPT]
android.permission.WRITE_EXTERNAL_STORAGE: granted=false, flags=[ USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHEN_DENIED|RESTRICTION_INSTALLER_EXEMPT]

And yes, i do have required permissions in my AndroidManifest.xml

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

Expected behavior

Get write access when calling Permission.storage.request().

Reproduction steps

Call Permission.storage.request() and create a folder in an external storage.

Configuration

[√] Flutter (Channel stable, 2.2.3, on Microsoft Windows [Version 10.0.19042.1110], locale pl-PL)

Version: ^8.1.4+2

Platform:

  • 📱 iOS
  • 🤖 Android
@stijnmelis
Copy link

I have the same problem, so I'd like to know the solution as well.

@kamlesh9070
Copy link

kamlesh9070 commented Jul 31, 2021

I have the same problem,
For Android 11 , It is working
Facing issue with Android 10
so I'd like to know the solution as well.

@JDDV
Copy link
Contributor

JDDV commented Aug 5, 2021

Hi @0x-cell, I tried to reproduce the same issue that you have on both Android 11 and Android 9.
On Android 11 I just had to import import 'dart:io' for creating a folder, no extra permission was needed.
On Android 9 however, you do need the storage permission.

I Wrote the following code:

void _createFolder() async {
    await Permission.storage.request();
    final folderName = "Test";
    final path = Directory("storage/emulated/0/Download/$folderName");

    if(await path.exists()){
      print('Exists');
    } else {
      print('Does not exist');
      path.create();
    }
  }

This made the folder named 'Test' in the Download folder, and only worked after requesting the Permission.storage.request() on Android 9.

On Android 10 it's a bit different, so I think @0x-cell and @Perrin666 and @kamlesh9070 you all have this issue on Android 10?

For Android 10 users you should ask the Permission.storage.request() permission, but also have besides

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

in your manifest, you also need android:requestLegacyExternalStorage="true" in your Android Manifest in the <application> tag. Because on Android 10 you no longer have access by default to arbitrary locations on external storage or removable storage. Adding the line mentioned above here in the manifest opts you into the legacy storage model, and the code snippet provided in this comment will work.

I'll close this issue for now since I think this will solve your problems, but let me know if it did not so I can re-open the issue!

@JDDV JDDV closed this as completed Aug 5, 2021
@stijnmelis
Copy link

stijnmelis commented Aug 11, 2021

Hi @JDDV,

Thanks for the answer but unfortunately it's not working for me. I tested this on 3 separate emulators (Android 9, 10 and 11), but I get the same behavior every time. I tried it on my actual phone as well, but no luck either (MotoG8+ Android 10).

This is my Android Manifest:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="xxx">
     <application
        android:allowBackup="false"
        android:fullBackupContent="false"
        android:requestLegacyExternalStorage="true"
        android:name="io.flutter.app.FlutterApplication"
        android:label="XXX"
        android:icon="@mipmap/logo">

        <meta-data android:name="com.google.android.geo.API_KEY"
            android:value="AIzaSyBnTwx_Q9UGiqG9GVtjh9lGH5VdwdRj13k"/>
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <meta-data
                android:name="io.flutter.embedding.android.SplashScreenDrawable"
                android:resource="@drawable/launch_background"
                />
            <meta-data
                android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
                android:value="true" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="FLUTTER_NOTIFICATION_CLICK" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>

And the relevant code for storing the file:

  static Future<String?> getDownloadDirectory() async {
    if (Platform.isAndroid) {
      String dirPath = '/storage/emulated/0/Download';

      // This should already exist, but just to make sure
      await Directory(dirPath).create(recursive: true);
      return dirPath;
    }

    // For iOS this one is OK
    final Directory? extDir = await getExternalStorageDirectory();
    return extDir?.path;
  }

  static Future<bool> requestStoragePermissions() async {
    PermissionStatus permission = await Permission.storage.status;

    if (permission != PermissionStatus.granted) {
      permission = await Permission.storage.request();
    }

    return permission == PermissionStatus.granted;
  }

  static Future<String?> writeDownloadedFile(Uint8List? attachment, String filename) async {
    if (attachment != null) {
      String? directory = await getDownloadDirectory();
      if (directory != null) {
        File attachmentFile = File(directory + "/" + filename);

        try {
          if (await requestStoragePermissions()) {
            if (!attachmentFile.existsSync()) {
              attachmentFile.create();
            }

            await attachmentFile.writeAsBytes(attachment, flush: true);

            return attachmentFile.path;
          }
        } catch (e) {

          return null;
        }
      }
    }
    return null;
  }

The problem is with the writeAsBytes call. I get the following error there:

image

Any idea what I am doing wrong?

Thanks!

@JDDV
Copy link
Contributor

JDDV commented Aug 12, 2021

@Perrin666 that's odd, I've tried to do it on Android 9, 10 and 11 with the code I've provided down here. I did use writeAsString instead of writeAsBytes, but that shouldn't make a difference.

Code I used to test
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @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, required this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  int _counter = 0;
  String _status = "Click to request permission";

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance!.removeObserver(this);
    super.dispose();
  }

  late AppLifecycleState _notification;

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _notification = state;
    });
    print(_notification);
  }

  void _askPermission() async {

    if (await Permission.storage.status == PermissionStatus.denied){
      await Permission.storage.request();
    }

    final folderName = "Test";
    final path = Directory('storage/emulated/0/Download/$folderName');

    if(await path.exists()){
      print('Exists');
      _writeInFile(path);
    } else {
      print('Does not exist');
      path.create();
    }

    setState(() {
      _counter++;
      _status;
    });
  }

  void _writeInFile(Directory path) async {
    // Get the file
    final textFile = await File(path.path + '/textFileForBytes.txt');
    textFile.writeAsString('This line gets written everytime.',flush: true);
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '$_status',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            onPressed: _askPermission,
            tooltip: 'Ask Permission',
            child: Icon(Icons.add),
          ),
        ],
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

It's the basic Flutter counter Application when you start a new Flutter Application project with a few extra methods.
I used the newest version of the plugin (version ^8.1.4+2), and for Android 10 I added in the manifest the thing I mentioned in my reaction earlier.

in your manifest, you also need android:requestLegacyExternalStorage="true" in your Android Manifest in the <application> tag. Because on Android 10 you no longer have access by default to arbitrary locations on external storage or removable storage. Adding the line mentioned above here in the manifest opts you into the legacy storage model, and the code snippet provided in this comment will work.

And of course I added the lines needed for asking for the permission in the Manifest.

<!-- Permissions options for the `storage` group -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

This for me, as long as I don't debug, works for me (somehow debugging makes it not work).
When you push the '+' button in the bottom right corner it will create a folder the first time if it does not exist, and the second time you push the button it will create the file if it does not exist yet and write the string in the textfile. It's basically the same thing that you do, but for me it works on all the Android versions.

For your code, I don't see anything weird going on. I assume the code goes in the catch statement after this line: await attachmentFile.writeAsBytes(attachment, flush: true);, is that correct ?

I assume you do get the request() dialog, and you've allowed it?

I don't think updating to the newest version of the plugin, followed by a flutter clean and a flutter pub get might solve the problem, but you can always try that. Let me know if you know more!

@stijnmelis
Copy link

stijnmelis commented Aug 13, 2021

Hi @JDDV,

Good thing you mentioned debugging, because that's exactly what I do. I will try out your suggestions withouth debugging and let you know the result.

And to make sure: I indeed get the request dialog, and then I allow it.

Update: I manage to run the standalone test app and write a file. So there must be something misconfigured in my code. Time to look deeper.

Another update: I uninstalled the app, and started from scratch again, and now it works. Not sure what changed since the previous time, but I'm happy it works.

And yet another update: it worked, now it suddenly no longer works. But the problem is clearly on my side, so I guess you can still close this issue.

Thanks for the help!

@JDDV
Copy link
Contributor

JDDV commented Aug 14, 2021

@Perrin666 yes I would say this problem is not something that has to do with the permission handler. Good luck with finding the problem, I hope you find it soon!

@stijnmelis
Copy link

Hi @JDDV,

I managed to fix it by using getExternalStorageDirectory() (and getApplicationDocumentsDirectory() as a fall back) as a download directory instead of using /storage/emulated/0/Download.

I'm still not sure why it works in your app, and not in mine, but it works like this, so I'm done messing with it :P

Thanks for the help!

@gfb-47
Copy link

gfb-47 commented Feb 3, 2022

I'm facing the same issue, it works on some devices and doesn't work on another devices, mostly Motorola.

I think it's related to the path /storage/emulated/0/ we're using. I'd rather use path_provider for this instead of this hard-coded path, it seems that this PR will provide a download folder option on this plugin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants