You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android手动保存文件至Download目录权限问题求助

问题解决指南:Flutter Android 无法写入Download目录(errno=1)

问题重现

尝试将文件保存到Android的Download文件夹,因path_provider无getDownloadsDirectory方法,手动指定路径/storage/emulated/0/Download时出现错误:

Unhandled Exception: FileSystemException: Cannot copy file to 'storage/emulated/0/Download/filename', path = '/data/user/0/com.xx.x/files/filename.pdf' (OS Error: Operation not permitted, errno = 1)

已通过Permission.storage.request()申请存储权限,并在AndroidManifest.xml添加以下权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
       tools:ignore="ScopedStorage" />

核心原因

  1. Scoped Storage限制:Android 10(API 29)及以上启用了Scoped Storage,WRITE_EXTERNAL_STORAGE权限不再允许直接写入公共目录(如Download)。
  2. 权限请求错误:MANAGE_EXTERNAL_STORAGE权限无法通过Permission.storage请求,需要单独处理。
  3. path_provider的替代方法:path_provider本身提供了访问公共Download目录的方式,无需手动硬编码路径。

解决方案

方案1:使用path_provider的正确API访问Download目录

path_provider的getExternalStoragePublicDirectory方法可访问公共Download目录,需处理Android版本兼容性:

import 'package:path_provider/path_provider.dart';
import 'dart:io';

Future<String?> getDownloadsPath() async {
  if (Platform.isAndroid) {
    Directory? downloadsDir = await getExternalStoragePublicDirectory(
      StorageDirectory.downloads,
    );
    return downloadsDir?.path;
  }
  // iOS 可自行补充对应逻辑
  return null;
}

方案2:正确请求MANAGE_EXTERNAL_STORAGE权限

若必须使用手动路径,需单独请求MANAGE_EXTERNAL_STORAGE权限:

  1. 确保pubspec.yaml中permission_handler版本≥10.0.0
  2. 权限请求代码:
import 'package:permission_handler/permission_handler.dart';

Future<void> requestManageStoragePermission() async {
  if (await Permission.manageExternalStorage.isDenied) {
    await Permission.manageExternalStorage.request();
    // 权限被永久拒绝时,引导用户到设置页授权
    if (await Permission.manageExternalStorage.isPermanentlyDenied) {
      openAppSettings();
    }
  }
}

注意:该权限需要用户手动在应用设置中开启"允许管理所有文件",且Google Play对使用该权限的应用有严格审核要求,非必要不建议使用。

方案3:适配Scoped Storage(推荐,无需特殊权限)

Android 10+推荐使用MediaStore写入Download目录,无需特殊权限,示例如下:

Flutter端代码

import 'dart:io';
import 'package:flutter/services.dart';

Future<void> saveFileToDownloads() async {
  final File sourceFile = File('/data/user/0/com.xx.x/files/filename.pdf');
  final String fileName = 'filename.pdf';

  if (Platform.isAndroid) {
    const MethodChannel channel = MethodChannel('com.example.file_save');
    try {
      await channel.invokeMethod('saveToDownloads', {
        'filePath': sourceFile.path,
        'fileName': fileName,
      });
    } on PlatformException catch (e) {
      print('Error: ${e.message}');
    }
  }
}

Android原生代码(MainActivity.kt)

import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example.file_save"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "saveToDownloads") {
                val filePath = call.argument<String>("filePath")
                val fileName = call.argument<String>("fileName")
                if (filePath != null && fileName != null) {
                    val success = saveFileToDownloads(filePath, fileName)
                    result.success(success)
                } else {
                    result.error("INVALID_ARGUMENTS", "File path or name is null", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }

    private fun saveFileToDownloads(filePath: String, fileName: String): Boolean {
        val sourceFile = java.io.File(filePath)
        if (!sourceFile.exists()) return false

        val contentValues = ContentValues().apply {
            put(MediaStore.Downloads.DISPLAY_NAME, fileName)
            put(MediaStore.Downloads.MIME_TYPE, "application/pdf")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.Downloads.RELATIVE_PATH, "Download")
                put(MediaStore.Downloads.IS_PENDING, 1)
            }
        }

        val resolver = contentResolver
        val uri: Uri? = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
        uri?.let {
            resolver.openOutputStream(it)?.use { outputStream ->
                sourceFile.inputStream().use { inputStream ->
                    inputStream.copyTo(outputStream)
                }
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                contentValues.put(MediaStore.Downloads.IS_PENDING, 0)
                resolver.update(it, contentValues, null, null)
            }
            return true
        }
        return false
    }
}

该方式符合Scoped Storage规范,无权限风险,且能通过Google Play审核。

总结

  • 优先选择方案3适配Scoped Storage,避免权限问题和审核风险。
  • 若需直接写入公共目录,需正确请求MANAGE_EXTERNAL_STORAGE权限并引导用户授权。
  • 避免手动硬编码路径,使用path_provider的官方API获取目录。

内容的提问来源于stack exchange,提问作者Unknown

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 13:22:09