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

Avalonia跨平台应用Android端长期文件访问问题求助

Avalonia跨平台应用Android端图片选择与访问权限问题

问题描述

开发的Avalonia跨平台应用支持选择单/多张图片,存储路径并生成缩略图,桌面端运行正常,但Android端崩溃,提示需通过ACTION_OPEN_DOCUMENT或相关API获取权限。具体异常表现:

  • 同目录下仅部分图片可正常读取,多数图片在生成缩略图及后续访问时报错
  • 将targetSdkVersion改为33时,仍提示需要已废弃的READ_EXTERNAL_STORAGE权限
  • 希望避免使用风险较高的MANAGE_EXTERNAL_STORAGE权限

当前使用的文件选择代码:

public async void addLibraryFromFilesEvent(object sender, RoutedEventArgs args)
{
    // Get top level from the current control. Alternatively, you can use Window reference instead.
    var topLevel = TopLevel.GetTopLevel(this);

    // Start async operation to open the dialog.
    var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
    {
        Title = "Select one or more images",
        AllowMultiple = true,
        FileTypeFilter = [FilePickerFileTypes.ImageAll]
    });

    if (files.Count <= 0)
    {
        return;
    }

    LibraryItem library = new LibraryItem();

    // The list of paths to the selected files
    library.paths = new List<string>();

    for (int i = 0; i < files.Count; i++)
    {
        // Checking if the selected file is an image
        if (SessionManager.ImageExtensions.Contains(Path.GetExtension(files[i].Path.ToString()).ToUpperInvariant()))
        {
            library.paths.Add(files[i].Path.ToString());
        }
    }

    // Path to the first image selected, that i use to create a thumbnail
    string originalCoverImagePath = library.paths[0];
    library.coverImagePath = new Uri("path/to/library/" + library.id + Path.GetExtension(originalCoverImagePath)).AbsoluteUri;

    // I create an avalonia IStorageFile object in order to read the image
    IStorageFile originalCoverImage = await topLevel.StorageProvider.TryGetFileFromPathAsync(new Uri(originalCoverImagePath));

    using (Stream coverStream = await originalCoverImage.OpenReadAsync())
    {
        Bitmap cover = new Bitmap(coverStream);
        double width = cover.Size.Width;
        double height = cover.Size.Height;
        int destinationHeight = 200;

        double wRatio = width / height;
        // Create scaled bitmap for the thumbnail
        Bitmap libraryThumbnail = cover.CreateScaledBitmap(new PixelSize((int)(destinationHeight * wRatio), destinationHeight));

        Uri coverImageUri = new Uri(library.coverImagePath);
        IStorageFolder thumbnailFolder = await topLevel.StorageProvider.TryGetFolderFromPathAsync(new Uri(PreferencesHandler.thumbnailsFolderPath));

        libraryThumbnail.Save("path/to/library/" + library.id + Path.GetExtension(originalCoverImagePath));

        PreferencesHandler.writeLibraryFile();
    }
}

AndroidManifest配置的权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

<uses-sdk android:minSdkVersion="21"
          android:targetSdkVersion="31" />

异常原因分析

  1. Android存储权限机制限制:

    • Android 10(API 29)引入分区存储,直接通过文件路径访问外部存储文件会被权限隔离,即使申请READ_EXTERNAL_STORAGE也无法访问所有文件。
    • Android 13(API 33)废弃READ_EXTERNAL_STORAGE,改用READ_MEDIA_IMAGES,但代码中若仍通过路径直接访问文件,系统会误判需要旧权限。
    • 部分图片能访问是因为这些文件位于应用私有目录或媒体数据库已索引的特定位置,多数文件因权限隔离无法访问。
  2. Avalonia文件选择后的错误用法:

    • 通过OpenFilePickerAsync获取的IStorageFile已拥有临时访问权限,但将Path存入集合后,后续通过TryGetFileFromPathAsync尝试访问时,路径对应的文件可能已超出权限范围,导致访问失败。

解决方案(基于ACTION_OPEN_DOCUMENT规范,无需MANAGE_EXTERNAL_STORAGE)

1. 存储文件Uri而非路径

Android中ACTION_OPEN_DOCUMENT返回的是文件的Content Uri,而非真实路径,Avalonia的IStorageFile.Path属性在Android上不可靠。应直接存储IStorageFile.Uri的字符串形式:

// 修改存储逻辑,保存Uri字符串而非路径
library.uris = new List<string>();
for (int i = 0; i < files.Count; i++)
{
    if (SessionManager.ImageExtensions.Contains(Path.GetExtension(files[i].Name).ToUpperInvariant()))
    {
        library.uris.Add(files[i].Uri.ToString());
    }
}

2. 后续访问直接通过Uri获取文件

需要访问图片时,直接用存储的Uri获取IStorageFile,无需通过路径:

// 替换原有的TryGetFileFromPathAsync逻辑
string originalCoverImageUriStr = library.uris[0];
IStorageFile originalCoverImage = await topLevel.StorageProvider.TryGetFileFromUriAsync(new Uri(originalCoverImageUriStr));

3. 调整Android权限配置

  • 移除MANAGE_EXTERNAL_STORAGE权限
  • 保留READ_MEDIA_IMAGES权限适配Android 13+
  • 兼容Android 12及以下时,添加READ_EXTERNAL_STORAGE并设置maxSdkVersion="32":
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

<uses-sdk android:minSdkVersion="21"
          android:targetSdkVersion="33" />

4. 缩略图保存到应用私有目录

避免将缩略图保存到外部公共目录,改用应用私有存储目录(无需额外权限):

// 获取应用私有存储目录的Uri
IStorageFolder privateFolder = await topLevel.StorageProvider.TryGetFolderFromPathAsync(new Uri(Android.App.Application.Context.FilesDir.AbsolutePath));
// 保存缩略图到私有目录
await libraryThumbnail.SaveAsync(await privateFolder.CreateFileAsync($"{library.id}{Path.GetExtension(originalCoverImage.Name)}", CreationCollisionOption.ReplaceExisting));

5. 持久化访问权限(可选)

若需重启应用后仍能访问选中的文件,可通过平台特定代码获取持久权限:

#if ANDROID
var context = Android.App.Application.Context;
var uri = Android.Net.Uri.Parse(originalCoverImageUriStr);
context.ContentResolver.TakePersistableUriPermission(uri, Android.Content.ActivityFlags.GrantReadUriPermission);
#endif

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 04:21:04