Androidでファイルを扱う（社内勉強会用）

# 基本: java.io.Fileを使う

```java:java
File file = new File(context.getFilesDir(), "my-file.txt");
try (FileWriter writer = new FileWriter(file)) {
    writer.write("my-file.");
}

```

```kotlin:kotlin
File(context.filesDir, "my-file.txt").writer().use {
    it.write("my-file.")
}
```

Context.getFilesDirを親フォルダに指定することで、自分のアプリ用の内部ディレクトリ＝『内部ストレージ』に保存する。
アプリケーションIDのフォルダ内に格納される。

```shell
# ls /data/data/com.access_company.file_demo/files/
my-file.txt
# cat /data/data/com.access_company.file_demo/files/my-file.txt
my-file.
```

---

# サブフォルダ

- Context.getFilesDir()
    - ./files
    - 通常のファイルを置く
    - 基本的に、ルートフォルダではなくこの files フォルダを使う
- Context.getCacheDir()
    - ./cache
    - 一時キャッシュファイル用
    - 不要になったら自分で消すこと
    - ただし、システムはストレージが不足し始めた場合、警告なしで削除することがある
    - ユーザ操作でも削除することができる [Screenshot](https://qiita-image-store.s3.amazonaws.com/0/107584/36948e2b-fdf2-3f09-2e2d-7ebcb54e6046.png)

```shell
# ls /data/data/com.access_company.file_demo/
cache files shared_prefs
```

※ shared_prefs については次のページ

---
# Android APIがその他のサブフォルダを利用する例: SharedPreference

```kotlin
// my-prefという名前のKVS
val pref = context.getSharedPreferences("my-pref", Context.MODE_PRIVATE)

// 書く
pref.edit {
    putBoolean("launched", true)
}

// 読む
val launched = pref.getBoolean("launched", false)
```

```shell
# ls /data/data/com.access_company.file_demo/shared_prefs
my-pref.xml
```

```shell
# cat /data/data/com.access_company.file_demo/shared_prefs/my-pref.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <boolean name="launched" value="true" />
</map>
```

---

# 他のアプリから読めないのか？

アプリごとに LinuxユーザーID が異なり、他アプリにはread/write権限がない。

```shell
# ls -la /data/data/com.access_company.file_demo/
drwxrwx--x u0_a67   u0_a67            2018-03-26 17:34 cache
drwxrwx--x u0_a67   u0_a67            2018-03-26 17:34 files
drwxrwx--x u0_a67   u0_a67            2018-03-26 18:01 shared_prefs

# ls -la /data/data/
drwxr-x--x u0_a67   u0_a67            2018-03-26 14:46 com.access_company.file_demo
drwxr-x--x u0_a68   u0_a68            2018-03-26 14:50 com.access_company.file_demo3
drwxr-x--x u0_a0    u0_a0             2018-03-25 14:24 com.android.backupconfirm
drwxr-x--x u0_a18   u0_a18            2018-03-25 14:24 com.android.backuptester
drwxr-x--x u0_a20   u0_a20            2018-03-25 14:24 com.android.browser
```

---

# Context.openFileOutput で Mode を指定する

```kotlin
openFileOutput("file-world-writable.txt", Context.MODE_WORLD_WRITEABLE).writer().use {
    it.write("World writable.")
}

openFileOutput("file-world-readable.txt", Context.MODE_WORLD_READABLE).writer().use {
    it.write("World readable.")
}

openFileOutput("file-private.txt", Context.MODE_PRIVATE).writer().use {
    it.write("private.")
}
```

```shell
# ls -la /data/data/com.access_company.file_demo/file_demo/files
-rw-rw---- u0_a67   u0_a67          8 2018-03-26 18:01 file-private.txt
-rw-rw-r-- u0_a67   u0_a67         15 2018-03-26 18:01 file-world-readable.txt
-rw-rw--w- u0_a67   u0_a67         15 2018-03-26 18:01 file-world-writable.txt
-rw------- u0_a67   u0_a67          8 2018-03-26 18:01 my-file.txt
```

この方法は危険だということで、Android 7.0 (N) から、Context.MODE_PRIVATE 以外は使えなくなった。
実行時に SecurityException が投げられる。

---

# sharedUserId

- AndroidManifest.xml で android:sharedUserId に同じ値が指定されている
- 同じ証明書で署名されている

を満たすアプリは、同じ LinuxユーザーID で動作する。

```xml:file_demo
<?xml version="1.0" encoding="utf-8"?>
<manifest
    android:sharedUserId="com.access_company.file_demo"
    package="com.access_company.file_demo">
```

```xml:file_demo2
<?xml version="1.0" encoding="utf-8"?>
<manifest
    android:sharedUserId="com.access_company.file_demo"
    package="com.access_company.file_demo2">
```

```xml:file_demo3
<?xml version="1.0" encoding="utf-8"?>
<manifest
    package="com.access_company.file_demo3">
```

```shell
# ls -la /data/data/
drwxr-x--x u0_a67   u0_a67            2018-03-26 14:46 com.access_company.file_demo
drwxr-x--x u0_a67   u0_a67            2018-03-26 14:52 com.access_company.file_demo2
drwxr-x--x u0_a68   u0_a68            2018-03-26 14:50 com.access_company.file_demo3

```

---

# adb shell の実行権限

これまでの例は、Android 6.0 (Marshmallow) Emulator 上で adb shell によりルート権限で実行していたが、新しいAndroidバージョンでは、ルート権限では動かせず、内部ストレージを確認することすらできない。
ただし、デバッグ版アプリに限り、`run-as` コマンドで、そのアプリの権限で実行することができる。

```shell
$ ls /data/data/com.access_company.file_demo
ls: /data/data/com.access_company.file_demo: Permission denied

$ run-as com.access_company.file_demo
$ pwd
/data/data/com.access_company.file_demo
$ ls
cache files shared_prefs
```

---
# 外部ストレージ

```kotlin
File(context.getExternalFilesDir(null), "external-file.txt").writer().use {
    it.write("external-file.")
}
```
/storage/emulated/0/Android/data/com.access_company.file_demo/files/external-file.txt に出力される。

* Context.getExternalFilesDir()
* Context.getExternalCacheDir()

---
# 外部ストレージとは

* 初期の頃は主にSDカードなどの外部記憶が割り当てられていたため、この名になっているが、内部記憶装置であることが多い
* 当然、依然としてリムーバブルであることは想定され、マウントされているかのチェックはを事前に行うべき
    * `if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED)`
* フォルダによっては、読み書きにパーミッション（ユーザによるアクセス許可）が必要

---
# 外部ストレージ　プライベートファイル／パブリックファイル

* プライベートファイル: `Context.getExternalFilesDir()`
    * /storage/emulated/0/Android/data/com.access_company.file_demo/files/..
    * 読み書きにパーミッションは不要
    * WRITE_EXTERNAL_STORAGE パーミッションさえあればどのアプリからも読み書き可能
    * アプリuninstall時に一緒に消される
    * 一般に（ギャラリーアプリなどから）ユーザの目に見えることはない

* パブリックファイル: `Environment.getExternalStoragePublicDirectory()`
    * /storage/emulated/0/..
    * 他のアプリからも、見える触れるファイル
    * WRITE_EXTERNAL_STORAGE and/or READ_EXTERNAL_STORAGE パーミッションが必要

---
# 外部ストレージの読み書き権限

```shell
$ ls -la /storage/emulated/0/                                                                                                                     
drwxrwx--x  2 root sdcard_rw 4096 2018-03-26 19:22 Alarms
drwxrwx--x  4 root sdcard_rw 4096 2018-03-26 19:22 Android
drwxrwx--x  2 root sdcard_rw 4096 2018-03-26 19:22 DCIM
drwxrwx--x  2 root sdcard_rw 4096 2018-03-26 19:22 Download
drwxrwx--x  2 root sdcard_rw 4096 2018-03-26 19:22 Movies
```

パーミッションが付与されたアプリは `sdcard_rw` グループに追加されているのだと思う。
READパーミッションだけの場合はどうやってるんだろう。

---
# アプリ間のファイルの受け渡し

* パブリックな外部ストレージに置いちゃうと、一時的にでもどのアプリから読めちゃう
* それぞれのアプリは、別プロセスで動作する

といった問題があるので、`ContentProvider` を使うのが一般的。

ContentProvider は、連絡先DBのような構造化されたデータや、ファイルのようなデータストリームにも利用できる。
前者の話は、今回はパス。

---
# Uri

* ContentProvider で提供するコンテンツの識別子。
* `android.net.Uri` クラス
    * java.net.URIではない
    * java.net.URLでもない
* 例
    * content://com.google.contacts/123/entity
    * content://com.access_company.file_demop.fileprovider/files/my-file.txt
    * file:///storage/emulated/0/Download/my-file.txt

---
# FileProvider

FileをContentProvider経由で提供するためのユーティリティ

```kotlin
// 提供したいファイル（ただしプライベート）
val file = File(context.cacheDir, "my-file.txt")

// FileProviderで提供するためのUriを作る
val uri = FileProvider.getUriForFile(context, "com.access_company.file_demo.fileprovider", file).also {
    // その時、特定のアプリにUriへのREADを許可する
    context.grantUriPermission("com.target_app", it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

// 表示インテントを投げる
val intent = Intent(Intent.ACTION_VIEW).also { it.data = uri }
context.startActivity(intent)
```

こうすると、 `com.access_company.file_demo.fileprovider` として外部向けに公開してあるFileProviderが、プライベートファイル指定したアプリにだけ提供できる。

詳しいことは[ドキュメント](https://developer.android.com/reference/androidx/core/content/FileProvider)参照。

---
# Uriからファイルを読む際の注意点

```kotlin
// 他のアプリから受け取ったUriをopenして、
context.contentResolver.openInputStream(uri).use { input ->
    // アプリ内にキャッシュファイルとしてコピー
    val cacheFile = File(context.cacheDir, "cacheFile.txt")
    cacheFile.outputStream().use { output ->
        input.copyTo(output)
    }

    // チャットとして送信
    sendChatMessage(cacheFile)
}
```

ContentResolver.openInputStream は file: スキームもオープンしちゃうので、例えば

```kotlin
val file = File("/data/data/com.access_company.file_demo/databases", "sqlite.db") //アプリのDBファイル
val uri = Uri.fromFile(file) //ファイルをUriに
```

をそのまま処理しちゃうと、古い端末では、内部ファイルが流出することになる。

※ Android 7.0 (Nougat) からは、fileスキームのUriをIntentに載せると FileUriExposedException が投げられるようになった。

---
# まとめ

* 使い分け
    * 内部ストレージ
        * 他アプリから読み書きされない
        * filesとcacheを使い分ける
    * 外部ストレージ／プライベートファイル
        * 大容量コンテンツ向け
        * uninstallで削除される
        * 他アプリからも読まれうるが、ユーザは見えない
        * リムーバブル
    * 外部ストレージ／パブリックファイル
        * 保存用
* 他アプリとの共有
    * FileProviderを使う

---
# thanks!