华为设备上的FileProvider getUriForFile()错误

42

在我的应用程序中,当使用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>
配置路径的资源文件:
<?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.getExternalFilesDirscontext.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文档所述不一致,该文档指出返回的第一个路径与getExternalFilesDir(String)相同

这解释了问题,因为我的代码中使用了context.getExternalFilesDir,而FileProvider使用的是ContextCompat.getExternalFilesDirs


你是从哪里/怎么获取这个File的?/storage/<card name>看起来不正确。 - CommonsWare
请告诉我在此设备上getExternalStorageDirectory()返回的是什么。 - greenapps
我正在使用 Context.getExternalFileDir(null) 获取文件。从这些设备上的日志中,它可以返回 storage/sdcard1/、/storage/3565-3131/、/storage/73A8-8626/、/storage/864A-F3ED... - guillaume-tgl
4个回答

24

针对Android N的更新(保留原来的答案,已确认这种新方法在生产中可行):

正如您在更新中指出的那样,许多华为设备型号(例如KIW-L24、ALE-L21、ALE-L02、PLK-L01和其他各种型号)破坏了对ContextCompat#getExternalFilesDirs(String)的调用的安卓合同。
它们不会像默认情况下一样返回Context#getExternalFilesDir(String)(即默认条目),而是如果存在外部SD卡,则将第一个对象作为外部SD卡的路径返回到数组中的第一个位置。

通过打破这个排序合同,这些带有外部SD卡的华为设备将在调用FileProvider#getUriForFile(Context, String, File)时出现IllegalArgumentException并崩溃,对于external-files-path根目录。虽然有许多解决方案可以尝试解决此问题(例如编写自定义FileProvider实现),但我发现最简单的方法是捕获此问题并执行以下操作:

  • Pre-N:返回Uri#fromFile(File),但由于FileUriExposedException,在Android N及以上版本上将无法工作。
  • N:将文件复制到您的cache-path(请注意:如果在UI线程上执行,可能会导致ANRs),然后为复制的文件返回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上出现此错误配置的华为设备。

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
我们现在在华为设备上看到这种情况发生,且是在Android N系统上。 - Edward Dale
我可以确认,在安卓N系统上,华为设备存在这种错误配置,导致了多次崩溃。 - guillaume-tgl
我也开始遇到这个问题了。在上面提供了更新的答案,应该更全面地解决这个漏洞。 - wrb
IOUtils目前已被弃用,有什么替代方案? - David
@David input.copyTo(output) - Canato

6

我曾经遇到同样的问题,我的解决方法是始终使用 ContextCompat.getExternalFilesDirs 调用来构建作为 FileProvider 参数的File

这样你就不必使用上述任何解决方法了。换句话说,如果你可以控制用于调用 FileProviderFile参数,并且/或者你不在意文件最终可能保存在经典的 /storage/emulated/0/Android/data/ 文件夹之外(因为它们都只在同一个SD卡上),那么我建议你采用我所做的方式。

如果情况不同,则建议使用上面的答案和自定义getUriForFile实现。


ContextCompat.getExternalFilesDirs 返回一个文件数组。你会选哪一个?如果你还记得的话 :) - Ufkoku
1
@Ufkoku 首先应该选择第一个。但要注意处理数组为空的情况。 - art

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支持库版本中可能会出现问题,但我找不到其他解决方法来使用现有的FileProvider在次要外部存储(通常是SD卡)中提供文件。


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 中提供者标签的 android:authorites 获取权限

从 file_paths.xml 中外部路径标签的 name 获取外部路径名称


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接