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

Xamarin Android 13无法在下载目录读写文件求助

在Xamarin.Android中解决Android 13下载目录读写权限问题

Android 10引入的分区存储机制,到Android 13又进一步细化了权限控制,直接导致旧的外部存储读写逻辑失效。以下是针对Xamarin环境的完整解决方案:

一、权限配置与动态申请

1. 清单文件配置

首先在AndroidManifest.xml中添加对应权限,区分不同版本适配:

<!-- Android 10-12 兼容用读写权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- Android 13+ 媒体文件权限(若配置文件属于图片/音频/视频类型) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- 访问所有外部文件的特殊权限(针对非媒体配置文件,需用户手动授权) -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- Android 10-12 过渡用旧存储模型(可选,仅用于兼容旧逻辑) -->
<application android:requestLegacyExternalStorage="true" ...>

2. 动态权限申请代码

在Activity或Fragment中实现权限检查与申请逻辑:

private const int RequestStoragePermissions = 100;

public async Task VerifyStoragePermissions()
{
    if (Build.VERSION.SdkInt >= BuildVersionCodes.Tiramisu)
    {
        // 检查媒体权限(针对媒体类配置文件)
        var hasMediaPermissions = ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadMediaImages) == Permission.Granted
            && ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadMediaVideo) == Permission.Granted
            && ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadMediaAudio) == Permission.Granted;

        if (!hasMediaPermissions)
        {
            ActivityCompat.RequestPermissions(this, 
                new[] { Manifest.Permission.ReadMediaImages, Manifest.Permission.ReadMediaVideo, Manifest.Permission.ReadMediaAudio }, 
                RequestStoragePermissions);
        }
        // 非媒体文件需申请全文件访问权限
        else if (!Environment.IsExternalStorageManager)
        {
            var intent = new Intent(Settings.ActionManageAppAllFilesAccessPermission);
            intent.SetData(Android.Net.Uri.Parse($"package:{PackageName}"));
            StartActivityForResult(intent, RequestStoragePermissions);
        }
        else
        {
            PerformFileOperations();
        }
    }
    else if (Build.VERSION.SdkInt >= BuildVersionCodes.Q)
    {
        // Android 10-12 申请传统读写权限
        var hasReadPerm = ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadExternalStorage) == Permission.Granted;
        var hasWritePerm = ContextCompat.CheckSelfPermission(this, Manifest.Permission.WriteExternalStorage) == Permission.Granted;

        if (!hasReadPerm || !hasWritePerm)
        {
            ActivityCompat.RequestPermissions(this, 
                new[] { Manifest.Permission.ReadExternalStorage, Manifest.Permission.WriteExternalStorage }, 
                RequestStoragePermissions);
        }
        else
        {
            PerformFileOperations();
        }
    }
}

// 处理权限申请结果
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
    base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == RequestStoragePermissions)
    {
        if (grantResults.All(grant => grant == Permission.Granted))
        {
            PerformFileOperations();
        }
        else
        {
            Toast.MakeText(this, "缺少存储权限,无法操作配置文件", ToastLength.Long).Show();
        }
    }
}

// 处理全文件访问权限的授权结果
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
    base.OnActivityResult(requestCode, resultCode, data);
    if (requestCode == RequestStoragePermissions)
    {
        if (Environment.IsExternalStorageManager)
        {
            PerformFileOperations();
        }
        else
        {
            Toast.MakeText(this, "未授予全文件访问权限,无法操作配置文件", ToastLength.Long).Show();
        }
    }
}

二、下载目录的文件读写实现

1. 获取下载目录路径

var downloadDirectory = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads);
var configFileFullPath = Path.Combine(downloadDirectory.AbsolutePath, "app_config.ini");

2. 写入配置文件

public void SaveConfigFile(string configContent)
{
    try
    {
        using var writer = new StreamWriter(configFileFullPath, false);
        writer.Write(configContent);
        Toast.MakeText(this, "配置文件已保存至下载目录", ToastLength.Short).Show();
    }
    catch (Exception ex)
    {
        Toast.MakeText(this, $"保存失败:{ex.Message}", ToastLength.Long).Show();
    }
}

3. 读取配置文件

public string LoadConfigFile()
{
    try
    {
        if (!File.Exists(configFileFullPath))
        {
            return string.Empty;
        }
        using var reader = new StreamReader(configFileFullPath);
        return reader.ReadToEnd();
    }
    catch (Exception ex)
    {
        Toast.MakeText(this, $"读取失败:{ex.Message}", ToastLength.Long).Show();
        return string.Empty;
    }
}

三、更友好的替代方案:存储访问框架(SAF)

如果不想申请MANAGE_EXTERNAL_STORAGE(该权限需要用户跳转到系统设置,体验较差),可以使用SAF让用户主动选择下载目录,通过Uri进行文件操作:

private const int RequestPickDirectory = 101;

public void PromptUserSelectDownloadDir()
{
    var intent = new Intent(Intent.ActionOpenDocumentTree);
    // 初始定位到下载目录
    var downloadUri = Android.Net.Uri.Parse($"content://{Android.OS.Environment.DirectoryDownloads}");
    intent.PutExtra(DocumentsContract.ExtraInitialUri, downloadUri);
    StartActivityForResult(intent, RequestPickDirectory);
}

protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
    base.OnActivityResult(requestCode, resultCode, data);
    if (requestCode == RequestPickDirectory && resultCode == Result.Ok && data != null)
    {
        var selectedDirUri = data.Data;
        // 持久化Uri权限,确保重启后仍可访问
        ContentResolver.TakePersistableUriPermission(selectedDirUri, 
            Intent.FlagsGrantReadUriPermission | Intent.FlagsGrantWriteUriPermission);
        
        // 构建配置文件的Uri
        var configFileUri = DocumentsContract.BuildDocumentUriUsingTree(selectedDirUri, DocumentsContract.GetTreeDocumentId(selectedDirUri))
            .BuildUpon().AppendPath("app_config.ini").Build();
        
        // 写入示例
        try
        {
            using var outputStream = ContentResolver.OpenOutputStream(configFileUri);
            using var writer = new StreamWriter(outputStream);
            writer.Write("{\"theme\":\"dark\",\"autoSync\":true}");
            Toast.MakeText(this, "配置文件保存成功", ToastLength.Short).Show();
        }
        catch (Exception ex)
        {
            Toast.MakeText(this, $"保存失败:{ex.Message}", ToastLength.Long).Show();
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 06:37:03