Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot write in /storage/emulated/0/Download #145

Closed
1 of 2 tasks
jeanchernandez opened this issue Aug 24, 2019 · 7 comments
Closed
1 of 2 tasks

Cannot write in /storage/emulated/0/Download #145

jeanchernandez opened this issue Aug 24, 2019 · 7 comments

Comments

@jeanchernandez
Copy link

jeanchernandez commented Aug 24, 2019

馃悰 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:

  • 馃摫 iOS
  • 馃 Android
@b3dl4m
Copy link

b3dl4m commented Jan 13, 2020

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?

@mvanbeusekom
Copy link
Member

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

@ninjaasmoke
Copy link

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

@fifitie
Copy link

fifitie commented Nov 21, 2021

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);
  }
}

@mvanbeusekom
Copy link
Member

@fifitie looking at the code you dumped here it looks like you are using a very old (pre-5.0.0, we are currently at 8.3.0) version of the permission handler. I would suggest you update this first.

@LuisMiguelSS
Copy link

Having the same issue while at version >= 10.0.0

@CL-Ali
Copy link

CL-Ali commented Sep 22, 2022

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?

I am using redmi note 11 (api level 30) and got same issue

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

7 participants