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

Xamarin Forms Android动态文件打开失败问题求助

Xamarin Forms Android端FileProvider打开下载文件失败问题

问题现象

  • 文件通过浏览器成功下载到设备Download文件夹
  • 代码能正确检测到文件存在
  • 调用FileProvider生成Uri并唤起应用选择框后,无论选择哪个应用,均无法打开文件,但手动从Download文件夹打开文件完全正常

相关代码实现

PCL项目核心类

public class EssentialsService : IEssentialsService
{
    public async Task OpenLinkBrowser(string URL)
    {
        await Browser.OpenAsync(URL);
    }
    public async Task<bool> CheckIfFileExists(string FileName)
    {
        return await DependencyService.Get<IDevice>().CheckFileExists("Download", FileName);
    }
    public async Task OpenExistingFile(string FileName)
    {
        await DependencyService.Get<IDevice>().ViewFile("Download", FileName);
    }
    public async Task OpenAttachmentLink(dynamic attachment)
    {
        StringToFreeSizeImageURLConverter stringToFreeSizeImageURLConverter = new StringToFreeSizeImageURLConverter();
        var fullLinkURL = stringToFreeSizeImageURLConverter.Convert(attachment.AttachementURL, null, null, null);
        string fileName = attachment.ID + "-" + attachment.Description;
        if (await CheckIfFileExists(fileName))
        {
            await OpenExistingFile(fileName);
            return;
        }
        if (!BaseApiService.IsConnceted())
        {
            //无网络时显示弹窗
            await BasePage.Resolve<INavigationService>().ShowETEPopup(LanguageClass.GetInstance().CHECK_YOUR_CONNECTION
            , LanguageClass.GetInstance().OK);
            return;
        }
        await OpenLinkBrowser(fullLinkURL as string);
    }
}

Android端文件操作实现

public class DeviceAndroid : IDevice
{
    public Task<bool> CheckFileExists(string folderName, string fileName)
    {
        string pathToDownloadsFolder = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath;
        Java.IO.File myDir = new Java.IO.File(pathToDownloadsFolder + "/" + folderName);
        if (!myDir.Exists()) return Task.FromResult(false);
        Java.IO.File file = new Java.IO.File(myDir, fileName);
        return Task.FromResult(file.Exists());
    }

    public Task<bool> ViewFile(string folderName, string fileName)
    {
        string pathToDownloadsFolder = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath;
        Java.IO.File myDir = new Java.IO.File(pathToDownloadsFolder + "/" + folderName);
        if (!myDir.Exists()) return Task.FromResult(false);
        Java.IO.File file = new Java.IO.File(myDir, fileName);
        if (!file.Exists()) throw new Exception("Downloaded File Does Not Exist!");
        var packageName = Android.App.Application.Context.ApplicationContext.PackageName;
        Android.Net.Uri path = FileProvider.GetUriForFile(Android.App.Application.Context, Android.App.Application.Context.ApplicationContext.PackageName + ".provider", file);
        string extension = Android.Webkit.MimeTypeMap.GetFileExtensionFromUrl(Android.Net.Uri.FromFile(file).ToString()); 
        string mimeType = Android.Webkit.MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
        Intent intent = new Intent(Intent.ActionView);
        intent.PutExtra(Intent.ExtraStream, path);
        intent.SetDataAndType(path, mimeType);
        intent.AddFlags(ActivityFlags.NewTask);
        intent.AddFlags(ActivityFlags.GrantReadUriPermission);
        var chooserIntent = Intent.CreateChooser(intent, "Choose App");
        chooserIntent.SetFlags(ActivityFlags.ClearWhenTaskReset | ActivityFlags.NewTask | ActivityFlags.ClearTop | ActivityFlags.GrantReadUriPermission);
        Android.App.Application.Context.StartActivity(chooserIntent);
        return Task.FromResult(true);
    }
}

AndroidManifest配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="2.0.3281" package="@appname" android:installLocation="auto">
    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="31" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.FLASHLIGHT" />
    <uses-permission android:name="android.permission.NFC" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.BATTERY_STATS" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="com.symbol.emdk.permission.EMDK" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <!-- Request Bluetooth permissions on Android 12 and above (location not necessary). -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
    <application android:allowBackup="false" android:fullBackupContent="false" android:usesCleartextTraffic="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher">
        <uses-library android:name="com.symbol.emdk" android:required="false" />
        <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
        <uses-library android:name="org.apache.http.legacy" android:required="false" />
        <provider android:name="android.support.v4.content.FileProvider" android:authorities="@appname.fileprovider" android:exported="false" android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"></meta-data>
        </provider>
        <provider android:name="androidx.core.content.FileProvider" android:authorities="@appname.provider" android:exported="false" android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"></meta-data>
        </provider>
    </application>
</manifest>

file_paths.xml配置

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
  <external-files-path name="my_images" path="Pictures" />
  <external-files-path name="my_movies" path="Movies" />
    <external-path name="external_files" path="."/>
</paths>

问题排查与修复方案

1. 移除冗余的Intent参数

同时设置PutExtra(Intent.ExtraStream, path)和SetDataAndType(path, mimeType)会导致Intent参数冲突,删除intent.PutExtra(Intent.ExtraStream, path),只保留SetDataAndType来指定文件Uri和MIME类型。

2. 修正MIME类型获取方式

当前使用Uri.FromFile(file)获取扩展名,Android 7.0+后该方法会触发FileUriExposedException,且可能无法正确解析文件名。改为直接从文件对象提取扩展名:

// 替换原扩展名获取代码
string extension = Java.IO.FileUtils.GetExtension(file.Name);
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
// 兜底处理未知MIME类型
if (string.IsNullOrEmpty(mimeType))
{
    mimeType = "application/octet-stream";
}

3. 完善权限授予逻辑

仅给Intent添加GrantReadUriPermission可能无法覆盖所有选择的应用,需要批量给匹配该Intent的应用授予Uri访问权限:

// 在创建chooserIntent前添加以下代码
var resInfoList = Android.App.Application.Context.PackageManager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
foreach (var resInfo in resInfoList)
{
    var appPackage = resInfo.ActivityInfo.PackageName;
    Android.App.Application.Context.GrantUriPermission(appPackage, path, ActivityFlags.GrantReadUriPermission);
}

4. 清理重复的FileProvider注册

AndroidManifest中同时注册了support和AndroidX版本的FileProvider,保留其中一个即可(推荐使用AndroidX版本),删除另一个provider节点。

5. 简化ChooserIntent的Flags设置

不需要过多冗余的Flags,仅保留必要的ActivityFlags.NewTask即可:

var chooserIntent = Intent.CreateChooser(intent, "选择应用打开");
chooserIntent.AddFlags(ActivityFlags.NewTask);

修改后的ViewFile方法示例

public Task<bool> ViewFile(string folderName, string fileName)
{
    string pathToDownloadsFolder = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath;
    Java.IO.File myDir = new Java.IO.File(pathToDownloadsFolder + "/" + folderName);
    if (!myDir.Exists()) return Task.FromResult(false);
    Java.IO.File file = new Java.IO.File(myDir, fileName);
    if (!file.Exists()) throw new Exception("下载的文件不存在!");

    var packageName = Android.App.Application.Context.PackageName;
    Android.Net.Uri path = FileProvider.GetUriForFile(Android.App.Application.Context, packageName + ".provider", file);

    // 从文件名直接获取扩展名
    string extension = Java.IO.FileUtils.GetExtension(file.Name);
    string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
    if (string.IsNullOrEmpty(mimeType))
    {
        mimeType = "application/octet-stream";
    }

    Intent intent = new Intent(Intent.ActionView);
    intent.SetDataAndType(path, mimeType);
    intent.AddFlags(ActivityFlags.NewTask);
    intent.AddFlags(ActivityFlags.GrantReadUriPermission);

    // 给所有匹配的应用授予Uri读取权限
    var resInfoList = Android.App.Application.Context.PackageManager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
    foreach (var resInfo in resInfoList)
    {
        var appPackage = resInfo.ActivityInfo.PackageName;
        Android.App.Application.Context.GrantUriPermission(appPackage, path, ActivityFlags.GrantReadUriPermission);
    }

    var chooserIntent = Intent.CreateChooser(intent, "选择应用打开");
    chooserIntent.AddFlags(ActivityFlags.NewTask);
    Android.App.Application.Context.StartActivity(chooserIntent);

    return Task.FromResult(true);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 01:40:26