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

Android 13及更高版本PDF保存权限被拒问题求助

解决Android 13 PDF保存权限被拒问题

问题概述

在Android 7设备上PDF保存功能正常,但切换到Android 13时出现权限错误:

Cannot open file, path = '/storage/emulated/0/Download/BitesVilla-invoice.pdf' (OS Error: Permission denied, errno = 13)

使用permission_handler插件请求存储权限始终返回Denied,修改AndroidManifest.xml也无法解决。

核心原因

  1. Android 10(API 29)启用分区存储,禁止直接通过硬编码路径写入公共外部存储目录;
  2. Android 13(API 33)进一步调整权限规则:保存文件到Download等公共目录无需申请READ/WRITE_EXTERNAL_STORAGE,但必须通过MediaStoreAPI操作,不能直接写入文件路径;
  3. 原代码中硬编码文件路径的方式在高版本系统中被拦截,导致权限拒绝。

解决方案

1. 替换文件保存逻辑为MediaStore API

修改_saveAsFile方法,使用MediaStore将PDF写入Download目录,无需申请存储权限:

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:pdf/widgets.dart' as pw;

Future<void> _saveAsFile(
  BuildContext context,
  pw.LayoutCallback build,
  pw.PdfPageFormat pageFormat,
) async {
  final bytes = await build(pageFormat);

  // 通过MethodChannel调用原生代码保存文件
  const channel = MethodChannel('com.your.app/file_save');
  try {
    await channel.invokeMethod('saveFileToDownload', {
      'fileName': 'BitesVilla-invoice.pdf',
      'bytes': bytes,
    });

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('PDF已保存到Download目录'),
        duration: Duration(seconds: 2),
      ),
    );
    await showNotification(context, File('/storage/emulated/0/Download/BitesVilla-invoice.pdf'));
  } on PlatformException catch (e) {
    logger.e('保存失败: ${e.message}');
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('保存失败: ${e.message}')),
    );
  }
}

在Android原生端添加MethodChannel实现(MainActivity.kt):

import android.content.ContentValues
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.your.app/file_save"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "saveFileToDownload") {
                val fileName = call.argument<String>("fileName")
                val bytes = call.argument<ByteArray>("bytes")
                if (fileName == null || bytes == null) {
                    result.error("INVALID_ARGUMENTS", "文件名或字节数据为空", null)
                    return@setMethodCallHandler
                }
                saveFileToDownload(fileName, bytes, result)
            } else {
                result.notImplemented()
            }
        }
    }

    private fun saveFileToDownload(fileName: String, bytes: ByteArray, result: MethodChannel.Result) {
        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 = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
        if (uri == null) {
            result.error("URI_NULL", "无法获取文件URI", null)
            return
        }

        try {
            resolver.openOutputStream(uri).use { outputStream ->
                outputStream?.write(bytes)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    contentValues.put(MediaStore.Downloads.IS_PENDING, 0)
                    resolver.update(uri, contentValues, null, null)
                }
            }
            result.success(true)
        } catch (e: Exception) {
            resolver.delete(uri, null, null)
            result.error("WRITE_FAILED", e.message, null)
        }
    }
}

2. 调整AndroidManifest.xml权限配置

移除或限制旧存储权限的作用范围,避免高版本系统的权限冲突:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<!-- 仅在Android 12及以下版本申请存储权限 -->
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />

3. 移除无效的权限请求代码

删除原代码中针对Android 13的存储权限请求逻辑,因为不需要这些权限:

// 以下代码可完全删除
// final value = await Permission.manageExternalStorage.request();
// logger.w("External Storage Value: $value");
// final status = await Permission.storage.status;
// logger.w("Permission: $status");
// if (Platform.isAndroid && androidInfo.version.sdkInt > 29) {
//   final val = await Permission.manageExternalStorage.request();
//   logger.w("Above 11: $val");
// } else {
//   final val = await Permission.storage.request();
//   logger.w("Below 11: $val");
// }

验证说明

  • Android 7及以下:兼容原逻辑或MediaStore方式,均可正常保存;
  • Android 10-12:通过maxSdkVersion限制的存储权限,配合MediaStore或原路径方式均可;
  • Android 13+:无需存储权限,通过MediaStore API直接写入Download目录,避免权限问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 02:52:14