Flutter-permission-handler: Cannot write in /storage/emulated/0/Download

Created on 24 Aug 2019  路  3Comments  路  Source: Baseflow/flutter-permission-handler

馃悰 Bug Report

On Android 9.

After permission granted I download a file and try to copy to /storage/emulated/0/Download without success, this is the error:

E/flutter ( 4663): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: FileSystemException: Cannot copy file to '/storage/emulated/0/Download/lorem-ipsum.pdf', path = '/data/user/0/com.example.tester/app_flutter/lorem-ipsum.pdf' (OS Error: Permission denied, errno = 13)
E/flutter ( 4663): #0 _File.copy. (dart:io/file_impl.dart:336:9)
E/flutter ( 4663): #1 _rootRunUnary (dart:async/zone.dart:1132:38)
E/flutter ( 4663): #2 _CustomZone.runUnary (dart:async/zone.dart:1029:19)
E/flutter ( 4663): #3 _FutureListener.handleValue (dart:async/future_impl.dart:126:18)
E/flutter ( 4663): #4 Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:639:45)
E/flutter ( 4663): #5 Future._propagateToListeners (dart:async/future_impl.dart:668:32)
E/flutter ( 4663): #6 Future._completeWithValue (dart:async/future_impl.dart:483:5)
E/flutter ( 4663): #7 Future._asyncComplete. (dart:async/future_impl.dart:513:7)
E/flutter ( 4663): #8 _rootRun (dart:async/zone.dart:1124:13)
E/flutter ( 4663): #9 _CustomZone.run (dart:async/zone.dart:1021:19)
E/flutter ( 4663): #10 _CustomZone.runGuarded (dart:async/zone.dart:923:7)
E/flutter ( 4663): #11 _CustomZone.bindCallbackGuarded. (dart:async/zone.dart:963:23)
E/flutter ( 4663): #12 _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
E/flutter ( 4663): #13 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)

Expected behavior

Copy file to /storage/emulated/0/Download

Reproduction steps

Use this as main.dart:

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:downloads_path_provider/downloads_path_provider.dart';
import 'package:dio/dio.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:progress_dialog/progress_dialog.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:open_file/open_file.dart';
import 'package:share_extend/share_extend.dart';

FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;

void main() async {
  flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

  /// Verificar si la app ha sido abierta por una notificaci贸n local agendada
  var notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
  print (notificationAppLaunchDetails);

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Prueba Descarga',
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  HomePage({Key key}) : super(key: key);

  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  Dio dio = Dio();
  String _url = "https://www.soundczech.cz/temp/lorem-ipsum.pdf";
  Directory _downloadsDirectory;
  ProgressDialog pr;
  double downloadProgress = 0.0;
  GlobalKey _scaffoldKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    _initDownloadsDirectoryState();
    _initLocalNotifications();

    pr = new ProgressDialog(context, ProgressDialogType.Download);
  }

  Future<void> _initDownloadsDirectoryState() async {
    Directory downloadsDirectory;
    if (Platform.isAndroid) {
      // with this fail:
      // downloadsDirectory = await DownloadsPathProvider.downloadsDirectory;
      downloadsDirectory = await getApplicationDocumentsDirectory();
    } else {
      downloadsDirectory = await getApplicationDocumentsDirectory();
    }
    if (!mounted) return;

    setState(() {
      _downloadsDirectory = downloadsDirectory;
    });
  }

  void _initLocalNotifications() {
    FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();

    var initializationSettingsAndroid = new AndroidInitializationSettings('app_icon');
    var initializationSettingsIOS = new IOSInitializationSettings(onDidReceiveLocalNotification: _onDidReceiveLocalNotification);

    var initializationSettings = new InitializationSettings(initializationSettingsAndroid, initializationSettingsIOS);
    flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: _onSelectNotification);
  }

  Future<dynamic> _onDidReceiveLocalNotification (int id, String title, String body, String payload) {
    print(id);
    print(title);
    print(body);
    print(payload);
    ShareExtend.share(payload, "file");
    return Future.value(payload);
  }

  Future<dynamic> _onSelectNotification(String payload) async {
    print(payload); // payload is the complete file url
    OpenFile.open(payload);
    return Future.value(payload);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: Text('Download test'),
      ),
      body: Center(
        child: Container(
          child: RaisedButton(
            child: Text('Download'),
            onPressed: () {
              _askForPermission().then(
                (_has) async {
                  if (_has) {
                    this._download();
                  }
                }
              );
            },
          )
        ),
      ),
    );
  }

  Future<bool> _askForPermission() async {
    bool result = true;
    Map<PermissionGroup, PermissionStatus> permissions;

    if (Platform.isAndroid) {
      PermissionStatus permission = await PermissionHandler().checkPermissionStatus(PermissionGroup.storage);
      if (permission!=PermissionStatus.granted){
        permissions = await PermissionHandler().requestPermissions([PermissionGroup.storage]);
        result = (permissions[PermissionGroup.storage]==PermissionStatus.granted);
      }
    }

    return Future.value(result);
  }

  /// Fires file's download
  void _download () async {
    this.downloadProgress = 0;
    if (mounted) { setState(() {}); }

    pr.show();    

    await dio.download(
      this._url,
      "${this._downloadsDirectory.path}/${this._url.split('/').last}",
      options: Options(
        headers: {HttpHeaders.acceptEncodingHeader: "*"}
      ),
      onReceiveProgress: (received, total) async {
        if (total != -1) {
          if (received == total) {
            print("Done!");
            this.downloadProgress = 0;
            pr.update(progress: 100.0, message: "100%");
            pr.hide();
            Future.delayed(Duration(seconds: 1), () {
              try {pr.update(progress: 0.0, message: "0%");} catch(e){}
            });

            var downloadsDirectory = await DownloadsPathProvider.downloadsDirectory;
            String filePath = "${downloadsDirectory.path}/${this._url.split('/').last}";
            try {
              File("${this._downloadsDirectory.path}/${this._url.split('/').last}").copy(filePath);
            } catch(e) {
              /// Always enters here
              print(e);
            }

            _notifyCompletedDownload(
              "File Downloaded",
              "File ${this._url.split('/').last} has been downloaded. Tap to open.",
              payload: "${this._downloadsDirectory.path}/${this._url.split('/').last}"
            );
          } else if (received < total) {
            /// Progress
            this.downloadProgress = (received / total * 100);
            print("${this.downloadProgress.toInt()}%");
            pr.update(progress: this.downloadProgress.toInt().toDouble(), message: "${this.downloadProgress.toInt()}%");
          }
        }
      }
    );
  }

  _notifyCompletedDownload(String title, String body, { String payload: "" }) async {
    var androidPlatformChannelSpecifics = AndroidNotificationDetails(
        '777', 'Downloads', 'Notification channel for downloads',
        importance: Importance.Max,
        priority: Priority.High,
        ticker: 'ticker'
    );
    var iOSPlatformChannelSpecifics = IOSNotificationDetails();
    var platformChannelSpecifics = NotificationDetails(
        androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);

    await flutterLocalNotificationsPlugin.show(0, title, body, platformChannelSpecifics, payload: payload);
  }
}

this is the dependency list:

  cupertino_icons: ^0.1.2
  permission_handler: ^3.2.2
  progress_dialog: ^1.1.0+1
  path_provider: ^1.1.2
  downloads_path_provider: ^0.1.0
  dio: ^2.1.13
  flutter_local_notifications: ^0.8.2
  share_extend: ^1.0.9
  open_file: ^2.0.2

This is the AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.tester">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.VIBRATE" />

    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="tester"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- This keeps the window background of the activity showing
                 until Flutter renders its first frame. It can be removed if
                 there is no splash screen (such as the default splash screen
                 defined in @style/LaunchTheme). -->
            <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>
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <provider
                android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
                android:authorities="${applicationId}.flutter_downloader.provider"
                android:exported="false"
                android:grantUriPermissions="true">
            <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/provider_paths"/>
        </provider>

        <provider
                android:name="androidx.work.impl.WorkManagerInitializer"
                android:authorities="${applicationId}.workmanager-init"
                android:enabled="false"
                android:exported="false" />

        <provider
                android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer"
                android:authorities="${applicationId}.flutter-downloader-init"
                android:exported="false">
            <meta-data
                    android:name="vn.hunghd.flutterdownloader.MAX_CONCURRENT_TASKS"
                    android:value="5" />
        </provider>

        <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"></action>
            </intent-filter>
        </receiver>

        <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
    </application>
</manifest>

Run, click the button, and the error appears.

Configuration

Version: 1.x

Platform:

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

Most helpful comment

This is an issue related to actual write permissions on the directory / disk you are trying to write the file too.

This has nothing to do with the user consent to allow an App to write to the disk. Similar issue was solved adding the following attribute to the <application> element in the AndroidManifest.xml file:

android:requestLegacyExternalStorage="true"

More information can also be found here: https://github.com/miguelpruivo/flutter_file_picker/issues/169

All 3 comments

I'm running into the same issue, but some of my testers are not. May I ask what Android device you're testing it on?

This is an issue related to actual write permissions on the directory / disk you are trying to write the file too.

This has nothing to do with the user consent to allow an App to write to the disk. Similar issue was solved adding the following attribute to the <application> element in the AndroidManifest.xml file:

android:requestLegacyExternalStorage="true"

More information can also be found here: https://github.com/miguelpruivo/flutter_file_picker/issues/169

This is an issue related to actual write permissions on the directory / disk you are trying to write the file too.

This has nothing to do with the user consent to allow an App to write to the disk. Similar issue was solved adding the following attribute to the <application> element in the AndroidManifest.xml file:

android:requestLegacyExternalStorage="true"

More information can also be found here: miguelpruivo/flutter_file_picker#169

Hey but the build fails if the minSdkVersion is not 29

Was this page helpful?
0 / 5 - 0 ratings