android - tutorial - Error FileProvider en dispositivos Huawei
photo android studio (3)
Tengo una excepción que ocurre solo en dispositivos Huawei en mi aplicación cuando uso 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)
Aquí está la definición de mi proveedor de archivos en mi manifiesto:
<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>
El archivo de recursos con rutas configuradas:
<?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>
¿Alguna idea sobre la causa de este problema y por qué ocurre solo en los dispositivos Huawei? ¿Cómo puedo depurar esto, dado que no tengo un dispositivo Huawei?
ACTUALIZAR:
He agregado más registros a mi aplicación y obtuve algunos resultados inconsistentes al imprimir tanto ContextCompat.getExternalFilesDirs
como context.getExternalFilesDir
en estos dispositivos:
ContextCompat.getExternalFilesDirs:
/storage/emulated/0/Android/data/<package>/files
/storage/sdcard1/Android/data/<package>/files
context.getExternalFilesDir:
/storage/sdcard1/Android/data/<package>/files
Esto es incoherente con la documentación de ContextCompat.getExternalFilesDirs
que indica que The first path returned is the same as getExternalFilesDir(String)
Eso explica el problema ya que uso context.getExternalFilesDir
en mi código y FileProvider
usa ContextCompat.getExternalFilesDirs
.
Mi solución a este problema ahora mismo, incluso si no es perfecta, es declarar mi FileProvider
con la siguiente ruta (para poder servir todos los archivos en el dispositivo):
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="" />
</paths>
Esto no está documentado oficialmente y puede fallar con una versión futura de la biblioteca de soporte de v4, pero no puedo ver ninguna otra solución para servir un archivo en el almacenamiento externo secundario (a menudo la tarjeta SD) usando el FileProvider
existente.
Tuve el mismo problema y mi solución al final fue usar siempre la llamada ContextCompat.getExternalFilesDirs
para compilar el File
que se usa como parámetro para FileProvider
. De esa manera, no tiene que usar ninguna de las soluciones anteriores.
En otras palabras. Si tiene control sobre el parámetro File
que utiliza para llamar a FileProvider
y / o no le importa que el archivo termine FileProvider
fuera de la carpeta classic /storage/emulated/0/Android/data/
(que debería estar guardada). perfectamente bien, ya que es la misma tarjeta SD), entonces sugiero hacer lo que he hecho.
Si no es su caso, sugiero utilizar la respuesta anterior con la implementación personalizada de getUriForFile
.
Actualización para Android N (dejando la respuesta original a continuación y he confirmado que este nuevo enfoque funciona en producción):
Como notó en su actualización, muchos modelos de dispositivos Huawei (por ejemplo, KIW-L24, ALE-L21, ALE-L02, PLK-L01 y una variedad de otros) rompen el contrato de Android para llamadas a ContextCompat#getExternalFilesDirs(String)
. En lugar de devolver Context#getExternalFilesDir(String)
(es decir, la entrada predeterminada) como el primer objeto de la matriz, devuelven el primer objeto como la ruta a la tarjeta SD externa, si hay uno.
Al romper este contrato de pedido, estos dispositivos Huawei con tarjetas SD externas se bloquearán con una IllegalArgumentException
en las llamadas a FileProvider#getUriForFile(Context, String, File)
para external-files-path
raíces de external-files-path
. Si bien hay una variedad de soluciones que puede buscar para tratar este problema (p. Ej., Escribir una implementación personalizada de FileProvider
), he encontrado que el enfoque más sencillo es detectar este problema y:
- Pre-N: devuelve
Uri#fromFile(File)
, que no funcionará con Android N y superior debido aFileUriExposedException
- N: Copie el archivo a su
cache-path
(nota: esto puede introducir ANR si se realiza en el subproceso de la interfaz de usuario) y luego devuelva elFileProvider#getUriForFile(Context, String, File)
para el archivo copiado (es decir, evitando el error por completo)
El código para lograr esto se puede encontrar a continuació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)) {
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);
}
}
}
Junto con el 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>
Una vez que haya creado una clase como esta, reemplace sus llamadas a:
FileProvider.getUriForFile(Context, String, File)
con:
ContentUriProvider.getUriForFile(Context, String, File)
Francamente, no creo que esta sea una solución especialmente elegante, pero nos permite usar el comportamiento de Android documentado formalmente sin hacer nada demasiado drástico (por ejemplo, escribir una implementación personalizada de FileProvider
). He probado esto en producción, así que puedo confirmar que resuelve estos bloqueos de Huawei. Para mí, este fue el mejor enfoque, ya que no deseaba dedicar demasiado tiempo a abordar lo que obviamente es un defecto del fabricante.
Actualice desde dispositivos anteriores de Huawei con este error actualizado a Android N:
Esto no funcionará con Android N y superior debido a FileUriExposedException
, pero aún no he encontrado un dispositivo Huawei con esta configuración FileUriExposedException
en 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);
}
}
}