Flutter集成Kotlin用ExifInterface写入图片元数据时权限拒绝问题
解决Flutter+Kotlin图片元数据写入权限拒绝问题
问题背景
开发一款可编辑图片元标签的Flutter应用,使用Kotlin通过MethodChannel处理元数据:
- 可正常读取
/storage/emulated/0/Download/05-03.jpg这类路径的图片元数据 - 写入元数据时触发权限拒绝错误:
java.io.FileNotFoundException: /storage/emulated/0/Download/05-03.jpg: open failed: EACCES (Permission denied) - 目标SDK版本为32,Manifest已添加存储相关权限,Flutter端也请求了存储权限,但问题仍存在
核心原因
Android 10(API 29)引入分区存储机制,限制应用直接访问公共目录下的文件;即使添加了WRITE_EXTERNAL_STORAGE权限,在API 30+环境下也无法直接通过文件路径写入公共存储内的文件。同时权限请求逻辑和Manifest配置存在疏漏,导致实际未获取到有效写入权限。
解决方案步骤
1. 修正AndroidManifest权限配置
调整权限声明,适配不同Android版本的权限规则,并启用传统存储兼容:
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- 仅在Android 29及以下版本请求WRITE权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" <!-- 针对Android 10+启用传统存储访问,兼容旧版文件操作逻辑 --> android:requestLegacyExternalStorage="true"> ... </application>
2. 优化Flutter端权限请求逻辑
根据Android版本动态请求对应权限,确保获取到正确的写入权限:
import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; Future<void> requestPermission() async { Permission targetPermission; final androidInfo = await DeviceInfoPlugin().androidInfo; if (androidInfo.version.sdkInt >= 33) { // Android 13+ 使用READ_MEDIA_IMAGES权限 targetPermission = Permission.photos; } else if (androidInfo.version.sdkInt >= 29) { // Android 10-12 使用存储权限 targetPermission = Permission.storage; } else { // Android 9及以下 使用WRITE_EXTERNAL_STORAGE权限 targetPermission = Permission.writeExternalStorage; } final status = await targetPermission.request(); if (status == PermissionStatus.granted) { setState(() { _permissionsGranted = true; }); _loadAlbums(); } else { print('存储权限未授予,无法编辑图片元数据'); } }
3. 修改Kotlin端写入逻辑,使用MediaStore访问文件
放弃直接通过文件路径写入,改用MediaStore获取文件的Content URI,通过ContentResolver操作文件:
import android.content.ContentUris import android.content.ContentResolver import android.database.Cursor import android.net.Uri import android.os.Build import android.provider.MediaStore import java.io.File import java.io.FileInputStream import java.io.OutputStream // MethodChannel处理方法内的逻辑 val arguments = call.arguments() as Map<String, Any>? val imagePath = arguments?.get("imagePath") as String val xmpString = arguments?.get("XMPString") as String val file = File(imagePath) if (!file.exists()) { result.error("FILE_NOT_EXIST", "图片文件不存在", null) return } // 从MediaStore查询图片的Content URI val contentUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) } else { MediaStore.Images.Media.EXTERNAL_CONTENT_URI } val projection = arrayOf(MediaStore.Images.Media._ID) val selection = "${MediaStore.Images.Media.DATA} = ?" val selectionArgs = arrayOf(imagePath) val cursor: Cursor? = contentResolver.query(contentUri, projection, selection, selectionArgs, null) cursor?.use { if (it.moveToFirst()) { val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val targetUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) // 通过ContentResolver打开输出流写入文件 contentResolver.openOutputStream(targetUri)?.use { outputStream -> // 先复制原文件内容到输出流 val inputStream = FileInputStream(file) inputStream.copyTo(outputStream) inputStream.close() // 写入XMP元数据 val exif = ExifInterface(outputStream) exif.setAttribute(ExifInterface.TAG_XMP, xmpString) exif.saveAttributes() result.success(null) } ?: run { result.error("STREAM_ERROR", "无法打开文件输出流", null) } } else { result.error("NOT_FOUND_IN_MEDIASTORE", "图片未在MediaStore中注册", null) } } ?: run { result.error("QUERY_ERROR", "查询MediaStore失败", null) }
4. 修复MissingPluginException
日志中同时出现MissingPluginException,需确保:
- Flutter端MethodChannel的名称和Kotlin端完全一致,比如Flutter端的
com.broump.gallery_organiser_flutter/exif_interface要和Kotlin端注册的Channel名称完全匹配 - Kotlin端正确注册了
setRatingXMP方法,没有拼写错误
额外注意事项
- 测试时需在应用设置中确认权限已被授予
- 若后续将目标SDK升级到33+,需移除
READ_EXTERNAL_STORAGE权限,仅保留READ_MEDIA_IMAGES - 对于Android 10+设备,若未启用
requestLegacyExternalStorage,必须完全依赖MediaStore操作公共存储文件
内容的提问来源于stack exchange,提问作者Broump
相关产品推荐
相关产品推荐

