我在使用FileProvider.getUriForFile
时在我的应用中仅在华为设备上发生了:
Exception: java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/<card name>/Android/data/<app package>/files/.export/2016-10-06 13-22-33.pdf
at android.support.v4.content.FileProvider$SimplePathStrategy.getUriForFile(SourceFile:711)
at android.support.v4.content.FileProvider.getUriForFile(SourceFile:400)
以下是我的清单中文件提供程序的定义:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
包含已配置路径的资源文件:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="external_files" path="" />
</paths>
有关此问题的原因以及为何仅在华为设备上发生的任何想法?鉴于我没有华为设备,我该怎么调试呢?
更新
我在我的应用中添加了更多日志,在这些设备上同时打印ContextCompat.getExternalFilesDirs
和context.getExternalFilesDir
时,我得到了一些不一致的结果:
ContextCompat.getExternalFilesDirs:
/storage/emulated/0/Android/data/<package>/files
/storage/sdcard1/Android/data/<package>/files
context.getExternalFilesDir:
/storage/sdcard1/Android/data/<package>/files
这与ContextCompat.getExternalFilesDirs
The first path returned is the same as getExternalFilesDir(String)
文档不一致
这解释了这个问题,因为我在代码中使用了context.getExternalFilesDir
而FileProvider
使用了ContextCompat.getExternalFilesDirs
。
答案 0 :(得分:14)
Android N更新(原始答案如下,并确认此新方法适用于制作):
正如您在更新中所述,许多华为设备型号(例如KIW-L24,ALE-L21,ALE-L02,PLK-L01以及其他各种设备型号)违反了对ContextCompat#getExternalFilesDirs(String)
来电的Android合约。不是将Context#getExternalFilesDir(String)
(即默认条目)作为数组中的第一个对象返回,而是返回第一个对象作为外部SD卡的路径(如果存在)。
通过违反此订购合同,带有外部SD卡的这些华为设备将因IllegalArgumentException
对FileProvider#getUriForFile(Context, String, File)
根external-files-path
的呼叫而崩溃。虽然您可以尝试各种解决方案来尝试解决此问题(例如编写自定义FileProvider
实现),但我发现最简单的方法是捕获此问题并且:
Uri#fromFile(File)
,由于FileUriExposedException
cache-path
(注意:如果在UI线程上完成,可以引入ANR),然后返回FileProvider#getUriForFile(Context, String, File)
复制的文件(即完全避免错误)完成此任务的代码可以在下面找到:
public class ContentUriProvider {
private static final String HUAWEI_MANUFACTURER = "Huawei";
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, @NonNull File file) {
if (HUAWEI_MANUFACTURER.equalsIgnoreCase(Build.MANUFACTURER)) {
Log.w(ContentUriProvider.class.getSimpleName(), "Using a Huawei device Increased likelihood of failure...");
try {
return FileProvider.getUriForFile(context, authority, file);
} catch (IllegalArgumentException e) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Log.w(ContentUriProvider.class.getSimpleName(), "Returning Uri.fromFile to avoid Huawei 'external-files-path' bug for pre-N devices", e);
return Uri.fromFile(file);
} else {
Log.w(ContentUriProvider.class.getSimpleName(), "ANR Risk -- Copying the file the location cache to avoid Huawei 'external-files-path' bug for N+ devices", e);
// Note: Periodically clear this cache
final File cacheFolder = new File(context.getCacheDir(), HUAWEI_MANUFACTURER);
final File cacheLocation = new File(cacheFolder, file.getName());
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(file);
out = new FileOutputStream(cacheLocation); // appending output stream
IOUtils.copy(in, out);
Log.i(ContentUriProvider.class.getSimpleName(), "Completed Android N+ Huawei file copy. Attempting to return the cached file");
return FileProvider.getUriForFile(context, authority, cacheLocation);
} catch (IOException e1) {
Log.e(ContentUriProvider.class.getSimpleName(), "Failed to copy the Huawei file. Re-throwing exception", e1);
throw new IllegalArgumentException("Huawei devices are unsupported for Android N", e1);
} finally {
IOUtils.closeQuietly(in);
IOUtils.closeQuietly(out);
}
}
}
} else {
return FileProvider.getUriForFile(context, authority, file);
}
}
}
与file_provider_paths.xml
:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="public-files-path" path="." />
<cache-path name="private-cache-path" path="." />
</paths>
创建完这样的课程后,请将您的来电替换为:
FileProvider.getUriForFile(Context, String, File)
使用:
ContentUriProvider.getUriForFile(Context, String, File)
坦率地说,我认为这不是一个特别优雅的解决方案,但它确实允许我们使用正式记录的Android行为,而不会做任何过于激烈的事情(例如编写自定义FileProvider
实现)。我已经在生产中对此进行了测试,因此我可以确认它可以解决这些华为崩溃问题。对我来说,这是最好的方法,因为我不想花太多时间来解决制造商的缺陷。
将此错误更新为Android N之前的华为设备更新:
由于FileUriExposedException
,这不适用于Android N及以上版本,但我还没有在Android N上遇到这种配置错误的华为设备。
public class ContentUriProvider {
private static final String HUAWEI_MANUFACTURER = "Huawei";
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, @NonNull File file) {
if (HUAWEI_MANUFACTURER.equalsIgnoreCase(Build.MANUFACTURER) && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Log.w(ContentUriProvider.class.getSimpleName(), "Using a Huawei device on pre-N. Increased likelihood of failure...");
try {
return FileProvider.getUriForFile(context, authority, file);
} catch (IllegalArgumentException e) {
Log.w(ContentUriProvider.class.getSimpleName(), "Returning Uri.fromFile to avoid Huawei 'external-files-path' bug", e);
return Uri.fromFile(file);
}
} else {
return FileProvider.getUriForFile(context, authority, file);
}
}
}
答案 1 :(得分:4)
我遇到了同样的问题,我最终的解决方案是始终使用ContextCompat.getExternalFilesDirs
调用构建File
作为FileProvider
的参数。
这样您就不必使用上述任何变通方法。
换句话说。如果您可以控制用于调用File
的{{1}}参数和/或您不关心文件可能最终被保存在经典FileProvider
文件夹之外(应该是完美的,因为它只是相同的SD卡)然后我建议我做我做的。
如果不是您的情况,那么我建议您使用自定义/storage/emulated/0/Android/data/
实施的上述答案。
答案 2 :(得分:2)
我现在解决这个问题,即使它并不完美,也是使用以下路径声明我的FileProvider
(以便能够为设备上的所有文件提供服务):
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="" />
</paths>
这没有正式记录,可能会破坏v4支持库的未来版本,但我看不到任何其他解决方案使用现有的{{1}在辅助外部存储(通常是SD卡)中提供文件}}
答案 3 :(得分:0)
尝试手动提供uri
var fileUri:Uri
try{
fileUri = FileProvider.getUriForFile(
this,
"com.example.android.fileprovider",
it
)
} catch (e:Exception){
Log.w("fileProvider Exception","$e")
fileUri=Uri.parse("content://${authority}/${external-path name}/${file name}")
}
从AndroidManifest.xml内的provider标记中的android:authorites获取权限
从file_paths.xml内的外部路径标签中的名称中获取外部路径名称