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
相关产品推荐
相关产品推荐

