华为设备上的FileProvider错误

gui*_*tgl 35 android android-fileprovider huawei

我在使用时在我的应用程序中的华为设备上发生异常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)
Run Code Online (Sandbox Code Playgroud)

以下是我的清单中文件提供程序的定义:

<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>
Run Code Online (Sandbox Code Playgroud)

具有已配置路径的资源文件:

<?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>
Run Code Online (Sandbox Code Playgroud)

关于这个问题的原因以及它为什么只在华为设备上发生的任何想法?鉴于我没有华为设备,我该怎么调试呢?

更新:

我已经添加了更多的登录到我的应用程序,我得到了一些打印时都不一致的结果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
Run Code Online (Sandbox Code Playgroud)

这与该ContextCompat.getExternalFilesDirs州的文件不一致The first path returned is the same as getExternalFilesDir(String)

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

wrb*_*wrb 17

Android N的更新(留下原始答案,并确认这种新方法适用于生产):

正如您在更新中所述,许多华为设备型号(例如KIW-L24,ALE-L21,ALE-L02,PLK-L01以及其他各种型号)都违反了Android合约的来电ContextCompat#getExternalFilesDirs(String).它们不是返回Context#getExternalFilesDir(String)(即默认条目)作为数组中的第一个对象,而是返回第一个对象作为外部SD卡的路径(如果存在).

通过打破这种排序合约,与外接SD卡,这些华为的设备将有崩溃IllegalArgumentException的调用 FileProvider#getUriForFile(Context, String, File)external-files-path根.虽然您可以尝试各种解决方案来尝试解决此问题(例如编写自定义FileProvider实现),但我发现最简单的方法是捕获此问题并:

  • Pre-N:返回Uri#fromFile(File),因使用Android N及以上版本无效FileUriExposedException
  • N:将文件复制到您的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);
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

随着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>
Run Code Online (Sandbox Code Playgroud)

一旦你创建了这样的类,将你的调用替换为:

FileProvider.getUriForFile(Context, String, File)
Run Code Online (Sandbox Code Playgroud)

有:

ContentUriProvider.getUriForFile(Context, String, File)
Run Code Online (Sandbox Code Playgroud)

坦率地说,我不认为这是一个特别优雅的解决方案,但它确实允许我们使用正式记录的Android行为,而不会做太过激烈的事情(例如编写自定义FileProvider实现).我已经在生产中对此进行了测试,因此我可以确认它可以解决这些华为崩溃问题.对我来说,这是最好的方法,因为我不想花太多时间来解决制造商的缺陷.

将此错误更新为Android N的华为设备更新:

这不适用于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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


sim*_*dam 6

我遇到了同样的问题,最终我的解决方案是始终使用ContextCompat.getExternalFilesDirs调用来构建File用作FileProvider. 这样您就不必使用上述任何解决方法。

换句话说。如果您可以控制File用于调用的参数FileProvider和/或您不关心文件最终可能会保存在经典/storage/emulated/0/Android/data/文件夹之外(这应该没问题,因为它们都是同一张 SD 卡)然后我建议做我做过的。

如果不是您的情况,那么我建议将上述答案与自定义getUriForFile实现一起使用。