J'ai une exception qui se produit niquement sur les appareils Huawei dans mon application lors de l'utilisation de 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)
Voici la définition de mon fournisseur de fichiers dans mon manifeste:
<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>
Le fichier de ressources avec des chemins configurés:
<?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>
Une idée de la cause de ce problème et pourquoi cela ne se produit que sur les appareils Huawei? Comment pourrais-je déboguer cela, étant donné que je n'ai pas d'appareil Huawei?
MISE À JOUR:
J'ai ajouté plus de journaux dans mon application et j'ai obtenu des résultats incohérents lors de l'impression de ContextCompat.getExternalFilesDirs
Et context.getExternalFilesDir
Sur ces appareils:
ContextCompat.getExternalFilesDirs:
/storage/emulated/0/Android/data/<package>/files
/storage/sdcard1/Android/data/<package>/files
context.getExternalFilesDir:
/storage/sdcard1/Android/data/<package>/files
Ceci n'est pas cohérent avec la documentation de ContextCompat.getExternalFilesDirs
Qui indique que The first path returned is the same as getExternalFilesDir(String)
Cela explique le problème car j'utilise context.getExternalFilesDir
Dans mon code et FileProvider
utilise ContextCompat.getExternalFilesDirs
.
Mise à jour pour Android N (en laissant la réponse d'origine ci-dessous et en confirmant que cette nouvelle approche fonctionne en production):
Comme vous l'avez noté dans votre mise à jour, de nombreux modèles d'appareils Huawei (par exemple KIW-L24, ALE-L21, ALE-L02, PLK-L01, et une variété d'autres) rompent le Android contrat pour les appels Android à ContextCompat#getExternalFilesDirs(String)
. Plutôt que de renvoyer Context#getExternalFilesDir(String)
(c'est-à-dire l'entrée par défaut) comme premier objet du tableau, ils renvoient à la place le premier objet comme chemin vers la carte SD externe, le cas échéant est présent.
En rompant ce contrat de commande, ces appareils Huawei avec des cartes SD externes planteront avec un IllegalArgumentException
lors des appels à FileProvider#getUriForFile(Context, String, File)
pour les racines external-files-path
. Bien qu'il existe une variété de solutions que vous pouvez rechercher pour tenter de résoudre ce problème (par exemple, écrire une implémentation personnalisée de FileProvider
), j'ai trouvé l'approche la plus simple consiste à résoudre ce problème et:
Uri#fromFile(File)
, qui ne fonctionnera pas avec Android N et supérieur en raison de FileUriExposedException
cache-path
(Remarque: cela peut introduire des ANR si cela est fait sur le thread d'interface utilisateur), puis renvoyez FileProvider#getUriForFile(Context, String, File)
pour le fichier copié (c'est-à-dire en évitant complètement le bogue)Le code pour accomplir ceci peut être trouvé ci-dessous:
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);
}
}
}
Avec le 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>
Une fois que vous avez créé une classe comme celle-ci, remplacez vos appels à:
FileProvider.getUriForFile(Context, String, File)
avec:
ContentUriProvider.getUriForFile(Context, String, File)
Franchement, je ne pense pas que ce soit une solution particulièrement gracieuse, mais elle nous permet d'utiliser formellement un comportement Android sans rien faire de trop drastique (par exemple, écrire un FileProvider
personnalisé _ J'ai testé cela en production, donc je peux confirmer qu'il résout ces plantages de Huawei. Pour moi, c'était la meilleure approche, car je ne souhaitais pas passer trop de temps à résoudre ce qui est évidemment un défaut de fabricant.
Mise à jour antérieure aux appareils Huawei avec ce bug mis à jour vers Android N:
Cela ne fonctionnera pas avec Android N et plus en raison de FileUriExposedException
, mais je n'ai pas encore rencontré un appareil Huawei avec cette mauvaise configuration sur 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);
}
}
}
J'ai eu le même problème et ma solution à la fin était de toujours utiliser le ContextCompat.getExternalFilesDirs
appel pour construire le File
qui est utilisé comme paramètre pour FileProvider
. De cette façon, vous n'avez à utiliser aucune des solutions de contournement ci-dessus.
En d'autres termes. Si vous avez le contrôle sur le paramètre File
que vous utilisez pour appeler FileProvider
et/ou que vous ne vous souciez pas que le fichier finisse par être enregistré en dehors du classique /storage/emulated/0/Android/data/
dossier (qui devrait être parfaitement bien, car c'est tout simplement la même carte SD) alors je suggère de faire ce que j'ai fait.
Si ce n'est pas votre cas, je suggère d'utiliser la réponse ci-dessus avec une implémentation personnalisée de getUriForFile
.
Ma solution à ce problème en ce moment, même si elle n'est pas parfaite, est de déclarer mon FileProvider
avec le chemin suivant (pour pouvoir servir tous les fichiers sur l'appareil):
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:Android="http://schemas.Android.com/apk/res/Android">
<root-path name="root" path="" />
</paths>
Ce n'est pas officiellement documenté et pourrait rompre avec une future version de la bibliothèque de support v4 mais je ne vois aucune autre solution pour servir un fichier dans le stockage externe secondaire (souvent la carte SD) en utilisant le FileProvider
existant .
Essayez de fournir manuellement l'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}")
}
obtenir l'autorité d'Android: les autorités dans la balise de fournisseur dans AndroidManifest.xml
obtenir le nom du chemin externe à partir du nom dans la balise de chemin externe dans file_paths.xml