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

.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;
    }
}

问题分析与修复方案

核心问题点

  1. 权限请求逻辑错误

    • Android 12+分支中,MainThread.BeginInvokeOnMainThread内部的await无法更新外部的status变量,导致后续权限判断使用初始的Unknown状态。
    • 低版本Android分支错误请求了LocationWhenInUse权限,而非存储权限,提示信息也与需求完全不符。
  2. MANAGE_EXTERNAL_STORAGE权限的特殊性

    • 该权限无法通过常规弹窗申请,必须引导用户到应用设置页面手动开启。
    • Google Play对该权限审核严格,仅允许文件管理器、备份类等特定应用使用,普通图片浏览应用不建议申请。
  3. 直接遍历/storage违反分区存储规则

    • Android 10(API 29)及以上引入分区存储,READ_MEDIA_IMAGES仅允许访问媒体库中的图片,无法直接遍历整个/storage目录。
    • Android.OS.Environment.StorageDirectory.Path对应的目录在分区存储模式下无直接访问权限,而RootDirectory.Path是系统根目录,部分子目录有默认权限,但这不是正确的图片访问方式。

修复步骤

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;
    }
}
  • 使用MediaStore API从媒体库获取图片文件夹,符合分区存储规则,无需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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 10:27:15