Flutter で外部ストレージにアクセスするアプリを作るときは Android バージョンを考慮して『ひと手間』いるから注意してくれよな！

## TL;DR

`READ_EXTERNAL_STORAGE` パーミッションや `WRITE_EXTERNAL_STORAGE` パーミッションを必要としているアプリは、 `AndroidManifest.xml` に `android:requestLegacyExternalStorage="true"` を設定すれば OK です。

```xml:app/src/main/AndroidManifest.xml
<application
    android:name="io.flutter.app.FlutterApplication"
    android:label="YourAppName"
    android:icon="@mipmap/ic_launcher"
    android:requestLegacyExternalStorage="true">
```



## 事の発端

開発中のアプリで [image_picker](https://pub.dev/packages/image_picker) を使っていました。
エミュレーターでは問題なく動作していたのですが、いざ実機で動作確認をしてみると……

**「イメージをピックできない！？」**

となったわけです。



### 実はエラーが発生していた

デバッグして確認してみると、

> Unable to decode stream: java.io.FileNotFoundException: /storage/emulated/0/Pictures/any_image.jpg: open failed: EACCES (Permission denied)

というエラーが発生していました。

パーミッション？　と思い  `AndroidManifest.xml` に `<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />` を設定してみましたが結果は変わりませんでした。



## 原因

じつは [Issues](https://github.com/flutter/flutter/issues/41459) にあがっていました。
Android バージョンが原因のようです。



### 変更されていた外部ストレージへのアクセス方法

Android 10 （ API レベル 29 ）以降から外部ストレージへのアクセス管理が変更 [^1] になっているようです。

> ユーザーがファイルを詳細に管理して整理できるように、Android 10（API レベル 29）以降をターゲットとするアプリには、外部ストレージ デバイスに対する特別アクセス権限がデフォルトで付与されます（対象範囲別ストレージ）

とのことで、 `READ_EXTERNAL_STORAGE` パーミッションや `WRITE_EXTERNAL_STORAGE` パーミッションは不要になっています。

また、他のアプリが作成したファイルへのアクセスは、

> 1. アプリに `READ_EXTERNAL_STORAGE` パーミッションが付与されていること。
> 2. 対象ファイルが、明確に定義された次のいずれかのメディア コレクション内にあること。
>    - **写真** - 保存場所: [`MediaStore.Images`](https://developer.android.com/reference/android/provider/MediaStore.Images?hl=ja)
>    - **動画** - 保存場所: [`MediaStore.Video`](https://developer.android.com/reference/android/provider/MediaStore.Video?hl=ja)
>    - **オーディオ** ファイル - 保存場所: [`MediaStore.Audio`](https://developer.android.com/reference/android/provider/MediaStore.Audio?hl=ja)

のようになっています。



つまり、 Flutter のパッケージがこの対応についていっていないと外部ストレージにアクセスできない状況が発生するということです。

[^1]: 変更内容の詳細は [Android デベロッパー](https://developer.android.com/training/data-storage/files/external-scoped?hl=ja) に詳しく記載されています



## 対策

[Android デベロッパー](https://developer.android.com/training/data-storage/files/external-scoped?hl=ja#opt-out-of-scoped-storage) に次のような警告があがっていました。
![警告.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135956/fb2e91d3-3e55-b641-5747-417758c8129a.png)
つまり、いずれきれいにしたいけど今は微妙な状態だから気をつけてな！　ってことでしょうね。
だから上で紹介した Issues もオープン状態のままなわけで……。


そんな状態でも一応は対処方法があるようなので紹介します。



### 対象範囲別ストレージをオプトアウトする

#### Android 9（ API レベル 28 ）以前をターゲットにする

これはそのとおりですね。
アクセス管理変更前のバージョンを相手にすればエラーは起こりません。



#### `requestLegacyExternalStorage` の値を `true` に設定する

Android 10 以降をターゲットにしている場合は、 `AndroidManifest.xml` に `android:requestLegacyExternalStorage="true"` を設定すれば OK です。

```xml:app/src/main/AndroidManifest.xml
<application
    android:name="io.flutter.app.FlutterApplication"
    android:label="YourAppName"
    android:icon="@mipmap/ic_launcher"
    android:requestLegacyExternalStorage="true">
```



#### 両方のバージョンをターゲットにしたい場合は？

```xml:app/src/main/AndroidManifest.xml
<uses-permission 
	android:name="android.permission.READ_EXTERNAL_STORAGE"
	android:maxSdkVersion="18" />
```

`uses-permission` に `maxSdkVersion` 属性が追加されたようなので、 `18` を指定すれば良さそうです。
（ただ Flutter で利用している分には `maxSdkVersion` 属性を指定してなくても動きそうなんだよな…… :thinking::question: ）



## まとめ

Flutter の開発をしていると、エラーの解消に割と時間がかかります。
プラットフォーム依存のものが多かったり、そもそも情報が少ないのも理由かもしれません。

自分が遭遇したエラーで情報の少ないものはなるべく書き溜めていこうと思いますので、同じエラーで困っている人のお役に少しでも立てれば幸いです！