Ошибка FileProvider на устройствах Huawei

У меня есть исключение, которое происходит только на устройствах Huawei в моем приложении при использовании 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)

Вот определение моего файлового провайдера в моем манифесте:

<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

Это не согласуется с документацией ContextCompat.getExternalFilesDirs в котором говорится, что The first path returned is the same as getExternalFilesDir(String)

Это объясняет проблему, так как я использую context.getExternalFilesDir в моем коде и FileProvider использования ContextCompat.getExternalFilesDirs,

3 ответа

Обновление для Android N (оставив исходный ответ ниже и подтвердив, что этот новый подход работает в производстве):

Как вы отметили в своем обновлении, многие модели устройств Huawei (например, KIW-L24, ALE-L21, ALE-L02, PLK-L01 и многие другие) нарушают контракт Android на звонки в ContextCompat#getExternalFilesDirs(String), Вместо того, чтобы возвращаться Context#getExternalFilesDir(String) (то есть запись по умолчанию) в качестве первого объекта в массиве, вместо этого они возвращают первый объект в качестве пути к внешней SD-карте, если таковой имеется.

Нарушение этого контракта на заказ приводит к отказу устройств Huawei с внешними SD-картами IllegalArgumentException на звонки в FileProvider#getUriForFile(Context, String, File) за external-files-path корнеплоды. Хотя существует множество решений, которые вы можете использовать для решения этой проблемы (например, написание FileProvider реализации), я нашел самый простой подход, чтобы поймать эту проблему и:

  • Pre-N: возврат Uri#fromFile(File), который не будет работать с Android N и выше из-за FileUriExposedException
  • N: скопируйте файл на свой cache-path (примечание: это может ввести 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, но мне еще не приходилось сталкиваться с устройством Huawei с этой неправильной конфигурацией на 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);
        }
    }
}

У меня была та же проблема, и мое решение, в конце концов, было всегда использовать ContextCompat.getExternalFilesDirs позвонить, чтобы построить File который используется в качестве параметра для FileProvider, Таким образом, вам не нужно использовать ни один из описанных выше обходных путей.

Другими словами. Если у вас есть контроль над File параметр, который вы используете для вызова FileProvider и / или вас не волнует, что файл может быть сохранен за пределами классического /storage/emulated/0/Android/data/ папку (что должно быть совершенно нормально, так как это все та же SD-карта), то я предлагаю сделать то, что я сделал.

Если это не ваш случай, то я предлагаю использовать приведенный выше ответ с пользовательским getUriForFile реализация.

Мое решение этой проблемы прямо сейчас, даже если оно не идеально, это объявить 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, но я не вижу другого решения для обслуживания файла во вторичном внешнем хранилище (часто на SD-карте) с использованием существующей FileProvider,

Попробуйте вручную указать 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}")
                    }

получить полномочия от android:authorites в теге провайдера внутри AndroidManifest.xml

получить имя внешнего пути из имени в теге внешнего пути внутри file_paths.xml

Другие вопросы по тегам