android – 在Huawei设备上的FileProvider错误

我有一个例外,只有在我的应用程序中的华为设备上使用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的文档不一致,说明返回的第一个路径与getExternalFilesDir(String)相同,

这解释了我在代码中使用context.getExternalFilesDir并且FileProvider使用ContextCompat.getExternalFilesDirs的问题.

解决方法

更新Android N(留下原始答案,并已确认这种新方法在生产中有效):

正如您在更新中所指出的那样,许多华为设备型号(例如KIW-L24,ALE-L21,ALE-L02,PLK-L01以及其他各种)将打破AndroidCompany对ContextCompat#getExternalFilesDirs(String)的调用.而不是返回Context#getExternalFilesDir(String)(即认条目)作为数组中的第一个对象,而是返回第一个对象作为外部SD卡的路径(如果存在).

通过打破这种订购合同,这些具有外部SD卡的华为设备将在对外部文件路径根的FileProvider#getUriForFile(Context,String,File)的调用上与IllegalArgumentException异常崩溃.尽管您可以采取各种解决方案来尝试处理此问题(例如编写一个自定义FileProvider实现),但我发现最简单的方法是抓住这个问题:

> Pre-N:返回Uri#fromFile(File),由于FileUriExposedException,它不能与Android N及更高版本配合使用
> N:将文件复制到缓存路径(注意:如果在UI线程上完成,可以引入ANR),然后为复制的文件返回FileProvider#getUriForFile(Context,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,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,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,File)

有:

ContentUriProvider.getUriForFile(Context,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 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,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,file);
        }
    }
}

相关文章

Android性能优化——之控件的优化 前面讲了图像的优化,接下...
前言 上一篇已经讲了如何实现textView中粗字体效果,里面主要...
最近项目重构,涉及到了数据库和文件下载,发现GreenDao这个...
WebView加载页面的两种方式 一、加载网络页面 加载网络页面,...
给APP全局设置字体主要分为两个方面来介绍 一、给原生界面设...
前言 最近UI大牛出了一版新的效果图,按照IOS的效果做的,页...