Help us understand the problem. What is going on with this article?

Androidでファイルを扱う(社内勉強会用)

More than 1 year has passed since last update.

Androidでファイルを扱う(社内勉強会用)

by komitake
1 / 18

基本: java.io.Fileを使う

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

kotlin
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 で動作する。

file_demo
<?xml version="1.0" encoding="utf-8"?>
<manifest
    android:sharedUserId="com.access_company.file_demo"
    package="com.access_company.file_demo">
file_demo2
<?xml version="1.0" encoding="utf-8"?>
<manifest
    android:sharedUserId="com.access_company.file_demo"
    package="com.access_company.file_demo2">
file_demo3
<?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を使う

thanks!

komitake
access
SDNからセンサ、家電、電子書籍まで。ACCESSはあらゆるレイヤのデバイス、サービスを「繋げて」いきます。
http://jp.access-company.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした