.NET MAUI安卓设备访问/storage目录时出现权限拒绝问题
.NET MAUI Android图片文件夹访问权限拒绝问题
我正在开发一个基于.NET MAUI的安卓应用,用于访问图片文件夹。目前遇到的问题是:读取/storage下的文件夹时总是收到权限拒绝提示,且是在物理设备上运行,应用权限设置里显示权限已授予。
另外一个奇怪的现象:使用Android.OS.Environment.RootDirectory.Path可以列出所有文件夹及子文件夹,但使用返回/storage路径的Android.OS.Environment.StorageDirectory.Path时,就会触发权限拒绝异常。
我的相关代码
Android Manifest
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> </manifest>
权限请求类
public class RequestPermision { public static async Task RequestStoragePermissions() { if (DeviceInfo.Platform != DevicePlatform.Android) return; var status = PermissionStatus.Unknown; if (DeviceInfo.Version.Major >= 12) { status = await Permissions.CheckStatusAsync<ReadWriteStoragePerms>(); if (status == PermissionStatus.Granted) return; if (Permissions.ShouldShowRationale<ReadWriteStoragePerms>()) { await Shell.Current.DisplayAlert("Needs permissions", "BECAUSE!!!", "OK"); } MainThread.BeginInvokeOnMainThread(async() => { status = await Permissions.RequestAsync<ReadWriteStoragePerms>(); }); } else { status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>(); if (status == PermissionStatus.Granted) return; if (Permissions.ShouldShowRationale<Permissions.LocationWhenInUse>()) { await Shell.Current.DisplayAlert("Needs permissions", "BECAUSE!!!", "OK"); } status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>(); } MainThread.BeginInvokeOnMainThread(async () => { if (status != PermissionStatus.Granted) await App.Current.MainPage.DisplayAlert("Permission required", "Location permission is required for bluetooth scanning. " + "We do not store or use your location at all.", "OK"); }); } }
public class ReadWriteStoragePerms : Permissions.BasePlatformPermission { #if ANDROID public override (string androidPermission, bool isRuntime)[] RequiredPermissions => new List<(string androidPermission, bool isRuntime)> { ("android.permission.READ_MEDIA_IMAGES", true), ("android.permission.MANAGE_EXTERNAL_STORAGE", true), ("android.permission.READ_EXTERNAL_STORAGE", true), ("android.permission.WRITE_EXTERNAL_STORAGE", true) }.ToArray(); #endif }
图片文件夹列表类
public class ListFolderAndroid { public static List<string> ListFolder() { var pictureFolders = new List<string>(); // 获取外部存储的主图片目录 var picturesDirectory = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures); if (picturesDirectory != null && picturesDirectory.IsDirectory) { // 将主图片目录路径添加到列表 pictureFolders.Add(picturesDirectory.Path); pictureFolders.AddRange(GetImageDirectories()); } return pictureFolders; } public static List<string> GetImageDirectories() { List<string> directories = new List<string>(); string externalStorageDirectory = Android.OS.Environment.StorageDirectory.Path; TraverseDirectoriesForImages(new DirectoryInfo(externalStorageDirectory), directories); return directories; } private static void TraverseDirectoriesForImages(DirectoryInfo directory, List<string> imageDirectories) { try { foreach (var file in directory.GetFiles()) { if (IsImageFile(file)) { string directoryPath = directory.FullName; if (!imageDirectories.Contains(directoryPath)) { imageDirectories.Add(directoryPath); } break; } } foreach (var subDirectory in directory.GetDirectories()) { TraverseDirectoriesForImages(subDirectory, imageDirectories); } } catch (Exception ex) { throw new Exception(ex.Message); } } private static bool IsImageFile(FileInfo file) { string[] imageExtensions = new string[] { ".jpg", ".jpeg", ".png", ".bmp", ".gif" }; return Array.IndexOf(imageExtensions, file.Extension.ToLower()) != -1; } }
问题分析与修复方案
核心问题点
权限请求逻辑错误
- Android 12+分支中,
MainThread.BeginInvokeOnMainThread内部的await无法更新外部的status变量,导致后续权限判断使用初始的Unknown状态。 - 低版本Android分支错误请求了
LocationWhenInUse权限,而非存储权限,提示信息也与需求完全不符。
- Android 12+分支中,
MANAGE_EXTERNAL_STORAGE权限的特殊性
- 该权限无法通过常规弹窗申请,必须引导用户到应用设置页面手动开启。
- Google Play对该权限审核严格,仅允许文件管理器、备份类等特定应用使用,普通图片浏览应用不建议申请。
直接遍历/storage违反分区存储规则
- Android 10(API 29)及以上引入分区存储,
READ_MEDIA_IMAGES仅允许访问媒体库中的图片,无法直接遍历整个/storage目录。 Android.OS.Environment.StorageDirectory.Path对应的目录在分区存储模式下无直接访问权限,而RootDirectory.Path是系统根目录,部分子目录有默认权限,但这不是正确的图片访问方式。
- Android 10(API 29)及以上引入分区存储,
修复步骤
1. 修正权限请求逻辑
public class RequestPermission { public static async Task RequestStoragePermissions() { if (DeviceInfo.Platform != DevicePlatform.Android) return; PermissionStatus status = PermissionStatus.Unknown; if (DeviceInfo.Version.Major >= 13) { // Android 13+ 使用READ_MEDIA_IMAGES访问图片 status = await Permissions.CheckStatusAsync<Permissions.Media>(); if (status != PermissionStatus.Granted) { if (Permissions.ShouldShowRationale<Permissions.Media>()) { await Shell.Current.DisplayAlert("需要图片权限", "应用需要访问您的图片文件夹以展示内容", "确定"); } status = await Permissions.RequestAsync<Permissions.Media>(); } } else if (DeviceInfo.Version.Major >= 10) { // Android 10-12 使用READ_EXTERNAL_STORAGE status = await Permissions.CheckStatusAsync<Permissions.StorageRead>(); if (status != PermissionStatus.Granted) { if (Permissions.ShouldShowRationale<Permissions.StorageRead>()) { await Shell.Current.DisplayAlert("需要存储权限", "应用需要访问您的图片文件夹以展示内容", "确定"); } status = await Permissions.RequestAsync<Permissions.StorageRead>(); } } else { // Android 9及以下 使用读写存储权限 var readStatus = await Permissions.CheckStatusAsync<Permissions.StorageRead>(); var writeStatus = await Permissions.CheckStatusAsync<Permissions.StorageWrite>(); if (readStatus != PermissionStatus.Granted || writeStatus != PermissionStatus.Granted) { if (Permissions.ShouldShowRationale<Permissions.StorageRead>() || Permissions.ShouldShowRationale<Permissions.StorageWrite>()) { await Shell.Current.DisplayAlert("需要存储权限", "应用需要访问您的图片文件夹以展示内容", "确定"); } await Permissions.RequestAsync<Permissions.StorageRead>(); await Permissions.RequestAsync<Permissions.StorageWrite>(); status = (await Permissions.CheckStatusAsync<Permissions.StorageRead>() == PermissionStatus.Granted && await Permissions.CheckStatusAsync<Permissions.StorageWrite>() == PermissionStatus.Granted) ? PermissionStatus.Granted : PermissionStatus.Denied; } else { status = PermissionStatus.Granted; } } // 仅当必须遍历全存储时处理MANAGE_EXTERNAL_STORAGE if (DeviceInfo.Version.Major >= 11 && status == PermissionStatus.Granted) { if (!Android.OS.Environment.IsExternalStorageManager) { var result = await Shell.Current.DisplayAlert("需要全存储权限", "应用需要访问所有存储目录以查找图片,是否跳转到设置开启?", "是", "否"); if (result) { Intent intent = new Intent(Android.Provider.Settings.ActionManageAppAllFilesAccessPermission); intent.AddCategory(Intent.CategoryDefault); intent.SetData(Android.Net.Uri.Parse($"package:{Android.App.Application.Context.PackageName}")); Android.App.Application.Context.StartActivity(intent); } } } if (status != PermissionStatus.Granted) { await App.Current.MainPage.DisplayAlert("权限未授予", "无法访问图片文件夹,请授予必要权限", "确定"); } } }
- 移除自定义权限类,直接使用.NET MAUI内置权限类型,避免手动管理权限列表的错误。
- 按Android版本区分处理权限,适配不同系统的权限规则。
- 若必须遍历全存储,单独引导用户开启
MANAGE_EXTERNAL_STORAGE权限。
2. 修正图片文件夹获取逻辑(符合分区存储规则)
不要直接遍历文件系统,改用媒体库API获取图片所在文件夹:
using Android.Content; using Android.Database; using Android.Provider; using Android.Net; public class MediaLibraryHelper { public static List<string> GetImageFolders() { List<string> folderPaths = new List<string>(); Context context = Android.App.Application.Context; // 查询媒体库中的图片,按文件夹分组 string[] projection = new[] { MediaStore.Images.Media.Data, MediaStore.Images.Media.BucketDisplayName, MediaStore.Images.Media.BucketId }; string sortOrder = MediaStore.Images.Media.BucketDisplayName + " ASC"; using (ICursor cursor = context.ContentResolver.Query( MediaStore.Images.Media.ExternalContentUri, projection, null, null, sortOrder)) { if (cursor != null) { HashSet<string> bucketIds = new HashSet<string>(); while (cursor.MoveToNext()) { string bucketId = cursor.GetString(cursor.GetColumnIndexOrThrow(MediaStore.Images.Media.BucketId)); if (!bucketIds.Contains(bucketId)) { bucketIds.Add(bucketId); string imagePath = cursor.GetString(cursor.GetColumnIndexOrThrow(MediaStore.Images.Media.Data)); string folderPath = System.IO.Path.GetDirectoryName(imagePath); if (!folderPaths.Contains(folderPath)) { folderPaths.Add(folderPath); } } } cursor.Close(); } } // 添加系统默认图片目录 var defaultPictureDir = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures); if (defaultPictureDir != null && defaultPictureDir.Exists && !folderPaths.Contains(defaultPictureDir.Path)) { folderPaths.Add(defaultPictureDir.Path); } return folderPaths; } }
- 使用
MediaStoreAPI从媒体库获取图片文件夹,符合分区存储规则,无需MANAGE_EXTERNAL_STORAGE权限。 - 通过
BucketId去重,避免重复添加同一文件夹。
3. 调整Android Manifest
移除冗余权限,按版本控制权限生效范围:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <!-- Android 9及以下 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" /> <!-- Android 10-12 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:minSdkVersion="30" android:maxSdkVersion="32" /> <!-- Android 13+ --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:minSdkVersion="33" /> <!-- 仅当必须遍历全存储时添加 --> <!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> --> </manifest>
内容的提问来源于stack exchange,提问作者Leonardo Silva
相关产品推荐
相关产品推荐

