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

Android 13无法访问Download目录文件问题求助

解决Android 13下Flutter访问Download目录报错问题

问题根源

Android 13(API 33)对存储权限和访问逻辑做了核心调整:

  • READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE权限被废弃,公共存储访问受Scoped Storage严格限制,直接通过文件路径访问Download等公共目录会被拦截。
  • getExternalStorageDirectory()自API 29起已被标记为废弃,Android 13下该方法返回的路径无法直接映射到真实的公共Download目录。

可行解决方案

方案1:用存储访问框架(SAF)访问Download目录(官方推荐)

SAF通过用户授权的方式访问公共存储,无需特殊权限,适配全版本:

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

// 调用原生方法打开Download目录选择文件
Future<File?> pickDownloadFile() async {
  const channel = MethodChannel('com.example.storage/saf');
  try {
    final String? filePath = await channel.invokeMethod('pickDownloadFile');
    return filePath != null ? File(filePath) : null;
  } on PlatformException catch (e) {
    print(e.message);
    return null;
  }
}

对应的Android原生代码(MainActivity中):

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example.storage/saf"
    private val REQUEST_PICK_FILE = 1001

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            when (call.method) {
                "pickDownloadFile" -> {
                    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
                        addCategory(Intent.CATEGORY_OPENABLE)
                        type = "*/*"
                        // 默认打开Download目录
                        val downloadUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI
                        putExtra(DocumentsContract.EXTRA_INITIAL_URI, downloadUri)
                    }
                    startActivityForResult(intent, REQUEST_PICK_FILE)
                    result.success(null)
                }
                else -> result.notImplemented()
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_PICK_FILE && resultCode == RESULT_OK) {
            data?.data?.let { uri ->
                val filePath = getFilePathFromUri(uri)
                MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CHANNEL)
                    .invokeMethod("filePicked", filePath)
            }
        }
    }

    private fun getFilePathFromUri(uri: Uri): String? {
        val projection = arrayOf(MediaStore.Downloads.DATA)
        contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
            if (cursor.moveToFirst()) {
                val index = cursor.getColumnIndexOrThrow(MediaStore.Downloads.DATA)
                return cursor.getString(index)
            }
        }
        return null
    }
}

方案2:使用MANAGE_EXTERNAL_STORAGE权限(仅适合特殊场景)

如果必须通过文件路径直接访问,需完成以下步骤:

  1. 清单文件保留权限(添加tools忽略警告):
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
    tools:ignore="ScopedStorage" />
  1. 运行时引导用户到设置开启权限(该权限无法通过常规请求弹窗获取):
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:io';

Future<bool> checkManageStoragePermission() async {
  var status = await Permission.manageExternalStorage.status;
  if (!status.isGranted) {
    status = await Permission.manageExternalStorage.request();
    if (status.isDenied) {
      // 跳转到应用设置页面引导开启
      await launchUrl(Uri.parse('package:${Platform.packageName}/settings'));
    }
  }
  return status.isGranted;
}
  1. 修正路径获取逻辑:
import 'package:path_provider_ex/path_provider_ex.dart'; // 需要添加依赖
import 'dart:io';

Future<Directory?> getDownloadDir() async {
  if (Platform.isAndroid) {
    try {
      final storageDirs = await PathProviderEx.getStorageDirectories();
      return Directory('${storageDirs.first.path}/Download');
    } catch (e) {
      return null;
    }
  } else {
    return await getApplicationDocumentsDirectory();
  }
}

方案3:用MediaStore创建Download目录文件

针对创建文件到Download目录的场景,可通过MediaStore实现:

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

Future<bool> saveToDownload(String fileName, Uint8List fileContent) async {
  const channel = MethodChannel('com.example.storage/mediastore');
  try {
    return await channel.invokeMethod('saveToDownload', {
      'fileName': fileName,
      'content': fileContent,
    });
  } on PlatformException catch (e) {
    print(e.message);
    return false;
  }
}

Android原生代码:

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example.storage/mediastore"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            when (call.method) {
                "saveToDownload" -> {
                    val fileName = call.argument<String>("fileName") ?: run {
                        result.error("INVALID_NAME", "文件名不能为空", null)
                        return@setMethodCallHandler
                    }
                    val content = call.argument<ByteArray>("content") ?: run {
                        result.error("INVALID_CONTENT", "文件内容不能为空", null)
                        return@setMethodCallHandler
                    }

                    val values = ContentValues().apply {
                        put(MediaStore.Downloads.DISPLAY_NAME, fileName)
                        put(MediaStore.Downloads.MIME_TYPE, "text/csv")
                        put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
                    }

                    val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
                    uri?.let {
                        contentResolver.openOutputStream(it)?.use { stream ->
                            stream.write(content)
                            result.success(true)
                        } ?: result.error("WRITE_FAILED", "无法打开输出流", null)
                    } ?: result.error("INSERT_FAILED", "无法写入MediaStore", null)
                }
                else -> result.notImplemented()
            }
        }
    }
}

核心注意事项

  • 避免直接拼接getExternalStorageDirectory()的路径访问公共目录,Android 13下Scoped Storage会拦截此类操作。
  • MANAGE_EXTERNAL_STORAGE权限仅适合文件管理器类应用,普通应用使用SAF或MediaStore更合规,避免被Google Play拒审。
  • 测试时需确认Android 13设备已授予对应权限(MANAGE_EXTERNAL_STORAGE需手动到设置开启)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 04:31:01