针对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);
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);
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);
}
}
}
File
的?/storage/<card name>
看起来不正确。 - CommonsWareContext.getExternalFileDir(null)
获取文件。从这些设备上的日志中,它可以返回 storage/sdcard1/、/storage/3565-3131/、/storage/73A8-8626/、/storage/864A-F3ED... - guillaume-tgl