华为设备上的FileProvider错误 [英] FileProvider error onHuawei devices
问题描述
使用FileProvider.getUriForFile
时,我的应用中仅在华为设备上发生例外:
I have an exception that happens only on Huawei devices in my app when using 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)
这是清单中我的文件提供者的定义:
Here is the definition of my file provider in my manifest:
<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>
具有配置路径的资源文件:
The resource file with configured paths:
<?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>
关于此问题的起因的任何想法,以及为什么仅在华为设备上发生?如果我没有华为设备,该如何调试?
Any idea on the cause of this issue and why it happens only on Huawei devices? How would I debug this, given that I don't have a Huawei device?
更新:
我在应用程序中添加了更多日志,在这些设备上同时打印ContextCompat.getExternalFilesDirs
和context.getExternalFilesDir
时,结果不一致:
I've added more logs into my app and I got some inconsistent results when printing both ContextCompat.getExternalFilesDirs
and context.getExternalFilesDir
on these devices:
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)
This is inconsistent with the documentation of ContextCompat.getExternalFilesDirs
that states that The first path returned is the same as getExternalFilesDir(String)
这解释了这个问题,因为我在代码中使用了context.getExternalFilesDir
而FileProvider
使用了ContextCompat.getExternalFilesDirs
.
That explains the issue since I use context.getExternalFilesDir
in my code and FileProvider
uses ContextCompat.getExternalFilesDirs
.
推荐答案
Android N的更新(下面保留了原始答案,并确认了这种新方法可以在生产环境中使用):
正如您在更新中指出的那样,许多华为设备型号(例如KIW-L24,ALE-L21,ALE-L02,PLK-L01和其他各种设备)都违反了Android呼叫ContextCompat#getExternalFilesDirs(String)
的合同.如果存在数组,它们将返回第一个对象作为到外部SD卡的路径,而不是将Context#getExternalFilesDir(String)
(即默认条目)作为数组中的第一个对象返回.
As you noted in your update, many Huawei device models (e.g. KIW-L24, ALE-L21, ALE-L02, PLK-L01, and a variety of others) break the Android contract for calls to ContextCompat#getExternalFilesDirs(String)
.
Rather than returning Context#getExternalFilesDir(String)
(ie the default entry) as the first object in the
array, they instead return the first object as the path to the external SD card, if one is present.
通过违反此订购合同,这些带有外部SD卡的华为设备将在呼叫FileProvider#getUriForFile(Context, String, File)
的external-files-path
根目录时以IllegalArgumentException
崩溃.您可以尝试使用多种解决方案来尝试解决此问题(例如,编写自定义FileProvider
实现),但我发现最简单的方法是捕获此问题,并:
By breaking this ordering contract, these Huawei devices with external SD cards will crash with an IllegalArgumentException
on calls to FileProvider#getUriForFile(Context, String, File)
for external-files-path
roots. While there are a variety of solutions that you can pursue to attempt to deal with this issue (e.g. writing a custom FileProvider
implementation), I've found the easiest approach is to catch this issue and:
- Pre-N:返回
Uri#fromFile(File)
,由于FileUriExposedException
,它不适用于Android N及更高版本
- N:将文件复制到您的
cache-path
(注意:如果在UI线程上执行此操作,可能会引入ANR),然后为复制的文件返回FileProvider#getUriForFile(Context, String, File)
(即,完全避免了该错误)
- Pre-N: Return
Uri#fromFile(File)
, which won't work with Android N and above due toFileUriExposedException
- N: Copy the file to your
cache-path
(note: this can introduce ANRs if done on the UI Thread) and then returnFileProvider#getUriForFile(Context, String, File)
for the copied file (ie avoiding the bug altogether)
完成此操作的代码如下:
Code to accomplish this can be found below:
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>
创建了这样的类后,将您的调用替换为:
Once you've created a class like this, replace your calls to:
FileProvider.getUriForFile(Context, String, File)
具有:
ContentUriProvider.getUriForFile(Context, String, File)
坦率地说,我认为这不是一个特别优雅的解决方案,但是它确实允许我们使用正式记录的Android行为,而无需做任何过于激烈的事情(例如,编写自定义的FileProvider
实现).我已经在生产环境中进行了测试,因此可以确认它可以解决这些华为故障.对我来说,这是最好的方法,因为我不想花太多时间解决明显是制造商的缺陷.
Frankly, I don't think this is an especially graceful solution, but it does allow us to use formally documented Android behavior without doing anything too drastic (e.g. writing a custom FileProvider
implementation). I have tested this in production, so I can confirm that it resolves these Huawei crashes. For me, this was the best approach, since I didn't wish to spend too much time addressing what is quite obviously a manufacturer's defect.
从此问题之前的华为设备更新到Android N:
由于FileUriExposedException
,这不适用于Android N及更高版本,但是我还没有遇到华为设备在Android N上的这种错误配置.
This won't work with Android N and above due to FileUriExposedException
, but I have yet to encounter a Huawei device with this mis-configuration on 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);
}
}
}
这篇关于华为设备上的FileProvider错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!