Android N での file:// スキーマによるファイル共有の挙動

# この記事は何か
Android N ではアプリ間のファイル共有に大きな変更があり、`file://` を使った Intent によるファイル共有ができなくなりました。

この点についてサンプルアプリを作成し、実際の挙動を確認して記事にまとめました。

# まとめ

- 動作 OS が Android N
  - Android N 向けのアプリは `file://` を使った Intent によるファイル共有は不可能。
  - Android N 未満向けのアプリは、
      - 外部ストレージにおいたファイルは `file://` を使って共有できる
      - 内部ストレージにおいたファイルは `file://` を使って共有できない 
- 動作 OS が Android N 未満
  - Android N 向けのアプリも、Android 未満向けのアプリも
      - 外部ストレージにおいたファイルは `file://` を使って共有できる
      - 内部ストレージにおいたファイルは `file://` を使って共有できない   
- `content://` を使った共有はバージョンによらず可能
  - ただし、Google Photos は画像の編集に制限がある

# Android N での変更点
Android N ではパーミッションに関していくつか変更がありました。

- Android N 向けアプリのプライベートディレクトリのパーミッションは 0700 になりました。
- 所有者であってもこのパーミッションを緩和することはできません。
  - MODE_WORLD_READABLE や MODE_WORLD_WRITEABLE は利用できません。SecurityException が発生します。 
- DownloadManager においてプライベートディレクトリにダウンロードされたファイルは共有できません。
  - Android N 向けアプリでは COLUMN_LOCAL_FILENAME は利用できません。SecurityException が発生します。
  - Android N 未満向けアプリでは**外部ストレージに置かれたファイル**のみ COLUMN_LOCAL_FILENAME でアクセスできます。ただしこの方法は推奨されません。
- Android N 向けアプリでは `file://` を利用した Intent でのファイルの共有はできません。
  - `file://` を含む Intent がアプリから外部アプリに発行されると FileUriExposedException が発生します。

※ 上記は次の記事の抜粋です。
[動作の変更点 | Android Developers](https://developer.android.com/about/versions/nougat/android-7.0-changes.html?hl=ja)


# 実際の挙動
アプリ内にある画像を、外部画像編集アプリを使って編集するようなサンプルアプリを作りました。
Android N 向けにビルドした環境で、Android N 端末とそうでない端末での挙動を調べました。

編集アプリとしては LINE CAMERA, QuickPick, Google Photos を利用しています。



## 条件の詳細
**アプリ**

1. LINE Camera: 13.2.3
2. QuickPic: 4.6.9.1485
3. Google Photos:
  - Android 6.0.1: 2.4.0.138563255
  - Android 7.0: 2.4.0.1338833446

**端末**

1. Android N 端末：Nexus 9, Android 7.0
2. Android N 未満端末： Nexus 5, Android 6.0.1


**スキーム**

1. "file://" スキーム：Uri.fromFile() を使ってパスを "file://" に変換
2. "content://" スキーム：FileProvider を使ってパスを "content://" に変換

**ファイルの置き場**

1. 内部ストレージ：Context.getFilesDir() で取得出来る領域
2. 外部ストレージ：Context.getExternalFilesDir(null) で取得出来る領域
 
## サンプルコード
https://github.com/hshiozawa/FileProviderSample/blob/master/app/src/main/java/com/hjm/fileprovidersample/MainActivity.java

## 結果

### Android N 端末
|スキーム + ファイル置き場|LINE Camera|Quickpic|Google Photos|
|---|---|---|---|
|`content://` + 内部ストレージ|〇|〇|△|
|`content://` + 外部ストレージ|〇|〇|△|
|`file://` + 内部ストレージ|×|×|×|
|`file://` + 外部ストレージ|×|×|×|

### Android N 未満端末
|スキーム + ファイル置き場|LINE Camera|Quickpic|Google Photos|
|---|---|---|---|
|`content://` + 内部ストレージ|〇|〇|△|
|`content://` + 外部ストレージ|〇|〇|△|
|`file://` + 内部ストレージ|×|×|×|
|`file://` + 外部ストレージ|〇|〇|〇|

- 〇: 編集可能
- ×: 編集不可能
- △: 限定的に可能

# 考察
## Android N 端末とそうでない端末での違い
表をみると、Android N 端末とそうでない端末では「`file://` + 外部ストレージ」の行の挙動が変わっていることが分かります。これらの事実からドキュメントの通り、`file://` での共有が禁止されていることが分かります。

また、発生している例外もドキュメント通りの FileUriExposedException です。

```
android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/com.hjm.fileprovidersample/files/smartphone.png exposed beyond app through Intent.getData()
    at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
    at android.net.Uri.checkFileUriExposed(Uri.java:2346)
    at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)
    at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
    at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
    at android.app.Activity.startActivityForResult(Activity.java:4224)
    at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:48)
    at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:75)
    at android.app.Activity.startActivityForResult(Activity.java:4183)
    at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:856)
    at android.app.Activity.startActivity(Activity.java:4507)
    at android.app.Activity.startActivity(Activity.java:4475)
    at com.hjm.fileprovidersample.MainActivity.startEdit(MainActivity.java:166)
    at com.hjm.fileprovidersample.MainActivity.access$100(MainActivity.java:21)
    at com.hjm.fileprovidersample.MainActivity$10.onClick(MainActivity.java:137)
    at android.view.View.performClick(View.java:5609)
    at android.view.View$PerformClick.run(View.java:22259)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:154)
    at android.app.ActivityThread.main(ActivityThread.java:6077)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)
```

## 内部ストレージに関して
Android N 未満の時から内部ストレージに対して `file://` スキームでファイル共有はできません。

Android N での変更点は「所有者であってもこのパーミッションを緩和することはできません。」という点です。MODE_WORLD_READABLE や MODE_WORLD_WRITEABLE を設定して、共有することができなくなりました。

### Google Photos の編集について
表を見ると分かりますが、Google Photos では一部の場合のみ画像の編集が可能です。
単に FileProvider を使って `content://` スキームに変換しただけでは画像の編集ができませんでした。

このときのログは次のようになっています。（実際の content URI がどのようなものか確認できます）

```
I/ActivityManager: START u0 {act=android.intent.action.EDIT dat=content://com.hjm.fileprovidersample.fileprovider/root/storage/emulated/0/Android/data/com.hjm.fileprovidersample/files/smartphone.png typ=image/png flg=0x3 cmp=com.google.android.apps.photos/.photoeditor.intents.EditActivity} from uid 10182 on display 0

```

一方、QuickPic で写真を開き、「編集」で Google Photos を選ぶと編集が可能です。この時のログは次のようになります。

```
I/ActivityManager: START u0 {act=android.intent.action.EDIT dat=content://media/external/images/media/3251 typ=image/* flg=0x3000001 cmp=com.google.android.apps.photos/.photoeditor.intents.EditActivity (has extras)} from uid 10086 on display 0
```

これを見ると media store 上の content URI が指定されていることが分かります。

### media store への画像の登録が必要？
これらを考えると、Google Photos で写真の編集を行う場合 content URI を使う場合は media store に登録する必要がありそうです。

Google Photos での写真の送信は media store でない URI でも可能です。
これは LINE Camera や QuickPic が常に「編集後の画像をコピーして保存」するのに対して、Google Photos が「編集後の画像を上書き保存する」機能があることに関連しそうです。