Android10以降で共有ストレージに保存されたファイルの読み書きを実装したい(Kotlin)

バージョン毎にやり方が違う上に用途によってもやり方が違う。おまけにJava, Kotlinで分かれているせいでまともに調べられない。

やりたいこと
Android10以降で
Kotlinで
外部から(具体的に言うとWindowsPCからUSB経由でファイルのやり取り)ファイルのアクセスがしたい
ついでにShift-JIS対応

内部的にファイルを利用する




まずファイルのやりとりが発生せず内部的に保持するだけなら以下のコードでファイルの読み書きが可能。

        // ファイル読み込み
        // Charset.forName("MS932")でShift-jis読み込み。未指定の場合UTF-8
        applicationContext.openFileInput("test.txt").bufferedReader(Charset.forName("MS932"))
            .useLines { lines ->
                // 改行区切りで処理 空行無視
                for (line in lines) {
                    Log.i("develop:file", "line:$line")
                }
            }
            
        // ファイルの書き込み
        val filename = "test.txt"
        val fileContents = "Hello world!"
        applicationContext.openFileOutput(filename, Context.MODE_PRIVATE).use {
            it.write(fileContents.toByteArray())
        }
一応ファイルの中身を見ることは可能でパスは
/data/data/[アプリケーションのパッケージ名]/files でファイルの読み書きが可能。
ただしAndroidのDevice File Manager経由でかつDebug版のAPKからインストールしたアプリしか恐らく参照できない。


Android10以降で共有ストレージに保存されたファイルの読み書きを実装する



ユースケースとしては恐らく下記の「直接ファイルパスを使用するコードまたはライブラリからファイルにアクセスする」になる?わかりにくい。
https://developer.android.com/training/data-storage/use-cases?hl=ja#access-file-paths

パーミッションのリクエスト

Androoid10未満対応
アプリ固有ストレージを使用するので以下は完全に不要(一応メモを残しておく)


<application
    android:requestLegacyExternalStorage="true"

requestLegacyExternalStorageをtrueにすればAndroid10までは動作するが11以降が動かなくなる。

AndroidManifest.xml

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
WRITEがあれば自動でREADも有効になる(今回は不要)
        if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            == PackageManager.PERMISSION_GRANTED) {
            // Permission承認済み
            Log.i("develop", "外部読み書き可能")

        } else {
            // Permissionリクエスト
            requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                1)
        }
コード上で権限をリクエストする場合AndroidManifest.xmlに記述する必要はない(今回は不要)

上記のやり方で/sdcard以下(Windowsから確認出来るディレクトリ全て)へ自由にアクセスできるようになる(ただし最大でAndroid10まで)
上記は古いバージョンの情報、あるいは用途が違う?そこまで調べてはいない。


ファイルへ書き込む(Android10以降)




Android10以降は特定のディレクトリ(/sdcard/Android/data/[パッケージ名])に対してデフォルトでアクセス権が付与されている。
https://developer.android.com/training/data-storage/app-specific#external
前置きが長くなったが要するに下記コードだけで良い。
// ファイルパス
val fileName = "text.txt"
val filePath = File(getExternalFilesDir(""), fileName)
Log.i("APPFilePath", filePath.absolutePath)

val fileContents = "Hello world!"
filePath.bufferedWriter().use {
    it.write(fileContents)
}


ファイルパスはコード(absolutePath)から表示した場合/storage/emulated/0/Android/data/[パッケージ名]/files/text.txt
Device File Explorerから確認した場合/sdcard/Android/data/[パッケージ名]/files/text.txt
Windowから確認した場合PC\[端末名]\内部共有ストレージ\Android\data\[パッケージ名]\files\text.txt となる。

getExternalFilesDirに引数を指定するとfiles以下にディレクトリが作成される。


ファイルを読みこむ


val filePath = File(getExternalFilesDir(""), fileName)
Log.i("APPFilePath", filePath.absolutePath)
filePath.bufferedReader(Charset.forName("MS932")).use {
    val text = it.readText()
    Log.i("FileRead", text)
}

filePath.bufferedReader(Charset.forName("MS932")) で引数を指定することで文字コードを指定して読み書き可能(デフォルトUTF-8)

アプリのインストール時にフォルダが作成される。
ファイルを該当箇所に配置した後にアプリインストールをおこなっても問題なく機能する。
アプリをアンインストールした場合、フォルダごと削除される。


まとめると
Androoid10以上は専用の領域が割り当てられるのでコード上で完結する。
Android9未満に対応する場合は従来通りパーミッションのリクエストが必要。

今回は『別のアプリで保存したファイルを読み書きする』といったことはしていない。
その他の方法を取りたい場合は以下のユースケースから(微妙に分かりにくい)
https://developer.android.com/training/data-storage/use-cases?hl=ja


わかってさえいれば簡単に出来るがここまでくるのが難しすぎる。



参考




アプリデータとファイル (公式)
https://developer.android.com/guide/topics/data?hl=ja

対象範囲別ストレージを使用する
https://qiita.com/irgaly/items/0ce450dd77bd54fb560a


パーミッションリクエスト
https://codechacha.com/ja/android-request-runtime-permissions/
ファイル読み書き方法
https://www.techiedelight.com/ja/write-to-file-kotlin/


2022年9月19日月曜日