web-dev-qa-db-ja.com

HuaweiデバイスでのFileProviderエラー

_FileProvider.getUriForFile_を使用すると、アプリで例外が発生しますHuaweiデバイスのみ

_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)
_

これが私のマニフェストでのファイルプロバイダーの定義です。

_<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>
_

パスが設定されたリソースファイル:

_<?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>
_

この問題の原因と、Huaweiデバイスでのみ発生する理由を教えてください。 Huaweiデバイスを持っていない場合、これをどのようにデバッグしますか?

更新:

アプリにログを追加しましたが、これらのデバイスで_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
_

これは、The first path returned is the same as getExternalFilesDir(String)と記載されている_ContextCompat.getExternalFilesDirs_のドキュメントと一致しません。

私のコードでは_context.getExternalFilesDir_を使用しており、FileProviderは_ContextCompat.getExternalFilesDirs_を使用しているため、これは問題を説明しています。

36
guillaume-tgl

Android Nの更新(以下の元の回答はそのままにし、この新しいアプローチが本番環境で機能することを確認しました):

アップデートで述べたように、多くのHuaweiデバイスモデル(KIW-L24、ALE-L21、ALE-L02、PLK-L01、その他のさまざまなモデル)がAndroid通話の契約を破ります) ContextCompat#getExternalFilesDirs(String)に変換します。Context#getExternalFilesDir(String)(デフォルトのエントリ)を配列の最初のオブジェクトとして返すのではなく、外部SDカードへのパスとして最初のオブジェクトを返します(存在する場合)存在します。

この注文契約に違反すると、外部SDカードを搭載したこれらのHuaweiデバイスは、_external-files-path_ルートのFileProvider#getUriForFile(Context, String, File)の呼び出し時にIllegalArgumentExceptionでクラッシュします。この問題に対処するために実行できるさまざまな解決策がありますが(たとえば、カスタムのFileProvider実装を作成するなど)、この問題を検出するのが最も簡単な方法であることがわかりました。

  • 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);
        }
    }

}
_

_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実装を作成する)。これを本番環境でテストしたところ、Huaweiのクラッシュを解決できることを確認できました。明らかにメーカーの欠陥に対処するのにあまり時間をかけたくなかったので、これは私にとって最良のアプローチでした。

このバグがアップデートされたHuaweiデバイスより前のアップデート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);
        }
    }
}
_
17
wrb

私は同じ問題を抱えていて、最終的に私の解決策は常にContextCompat.getExternalFilesDirsFileのパラメータとして使用されるFileProviderを構築するための呼び出し。これにより、上記の回避策を使用する必要がなくなります。

言い換えると。 Fileの呼び出しに使用するFileProviderパラメーターを制御できる場合、および/またはファイルが従来の/storage/emulated/0/Android/data/フォルダー(すべて同じSDカードなので、これで問題ありません)を実行することをお勧めします。

それがあなたのケースでない場合は、カスタムのgetUriForFile実装で上記の回答を使用することをお勧めします。

5
simekadam

この問題の現在の解決策は、完璧ではない場合でも、次のパスを使用してFileProviderを宣言することです(デバイス上のすべてのファイルを提供できるようにするため)。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:Android="http://schemas.Android.com/apk/res/Android">
    <root-path name="root" path="" />
</paths>

これは正式には文書化されておらず、v4サポートライブラリの将来のバージョンで機能しなくなる可能性がありますが、既存のFileProviderを使用してセカンダリ外部ストレージ(多くの場合SDカード)でファイルを提供する他の解決策が見当たらない。

2
guillaume-tgl

手動で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}")
                    }

androidManifest.xml内のプロバイダータグでAndroid:authoritesから権限を取得する

file_paths.xml内の外部パスタグの名前から外部パス名を取得します

0
guest5618