基本: java.io.Fileを使う
File file = new File(context.getFilesDir(), "my-file.txt");
try (FileWriter writer = new FileWriter(file)) {
writer.write("my-file.");
}
File(context.filesDir, "my-file.txt").writer().use {
it.write("my-file.")
}
Context.getFilesDirを親フォルダに指定することで、自分のアプリ用の内部ディレクトリ=『内部ストレージ』に保存する。
アプリケーションIDのフォルダ内に格納される。
# 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
# ls /data/data/com.access_company.file_demo/
cache files shared_prefs
※ shared_prefs については次のページ
Android APIがその他のサブフォルダを利用する例: SharedPreference
// my-prefという名前のKVS
val pref = context.getSharedPreferences("my-pref", Context.MODE_PRIVATE)
// 書く
pref.edit {
putBoolean("launched", true)
}
// 読む
val launched = pref.getBoolean("launched", false)
# ls /data/data/com.access_company.file_demo/shared_prefs
my-pref.xml
# 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権限がない。
# 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 を指定する
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.")
}
# 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 version="1.0" encoding="utf-8"?>
<manifest
android:sharedUserId="com.access_company.file_demo"
package="com.access_company.file_demo">
<?xml version="1.0" encoding="utf-8"?>
<manifest
android:sharedUserId="com.access_company.file_demo"
package="com.access_company.file_demo2">
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.access_company.file_demo3">
# 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
コマンドで、そのアプリの権限で実行することができる。
$ 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
外部ストレージ
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 パーミッションが必要
外部ストレージの読み書き権限
$ 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経由で提供するためのユーティリティ
// 提供したいファイル(ただしプライベート)
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が、プライベートファイル指定したアプリにだけ提供できる。
詳しいことはドキュメント参照。
Uriからファイルを読む際の注意点
// 他のアプリから受け取った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: スキームもオープンしちゃうので、例えば
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を使う