web-dev-qa-db-fra.com

Erreur FileProvider sur les appareils Huawei

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.

36
guillaume-tgl

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:

  • Pre-N: Renvoie Uri#fromFile(File), qui ne fonctionnera pas avec Android N et supérieur en raison de FileUriExposedException
  • N: Copiez le fichier dans votre 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);
        }
    }
}
17
wrb

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.

5
simekadam

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 .

2
guillaume-tgl

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

0
guest5618