私が作成したAndroid用のあるアプリにはデータのインポート及びエクスポートの機能が付いています。
開発当時はAndroid 9が最新でそれに合わせた実装をしていて特に問題なく動いていました。
2022年1月、私がメインで使っているスマホはAndroid 12で、サブのスマホはAndroid 11です。
これらのスマホだと上記のインポート及びエクスポート機能が正常に動きませんでした。
Fireタブレット(Fire HD 10)はAndroid 9ベースのFire OS 7で、こちらだと問題なく動くのでAndroid 10以降に何かの仕様が変わったのではないかと思われます。
今まで気が付かなかったくらいなので当該機能の使用頻度は極めて低く、別になくてもいいような機能ではありますがデータのバックアップ・リストアを行う際には絶対に必要になるので修正をすることにしました。
結論から言うと「Environment.getExternalStorageDirectory」メソッドで取得したパスを使ってのファイル入出力ができなくなったことが原因です。
Android 10におけるプライバシーに関する主な変更点
公式サイトにも記述がある通り、Android 10になって「対象範囲別ストレージ」というものが導入されました。
対象範囲別ストレージには3種類あります。
- アプリ固有のストレージ
- メディアストレージ
- その他のストレージ
対象範囲別ストレージの詳細は割愛しますが、簡単にいうとアプリが好き勝手に「その他のストレージ」にアクセスできなくなってしまいました。
対応策
上記の「対象範囲別ストレージ」のうち、アプリ固有のストレージとメディアストレージには比較的自由にアプリ側からアクセスすることが可能です。
しかし、インポート及びエクスポートの機能をイメージしていただくと分かるとおり、ユーザーが作成したインポート用ファイルを読み込んだり、エクスポートしたファイルをユーザーが自分の好きなところに保存できたりすることは必須ですし、テキストファイルでの入出力を想定しているのでメディアストレージは使えない状況です。
どうにかして「その他のストレージ」にアクセスする必要があります。
そこで使うのが「ストレージアクセスフレームワーク」(SAF)です。
簡単にいうと、ユーザーに保存するファイルや読み込むファイルを指定してもらってアクセスする方法です。
Environment.getExternalStorageDirectoryメソッドでパスを取得する代わりに、ユーザーに指定してもらう感じになります。
サンプルソース(Kotlin)
エクスポート時(ファイル保存時)に保存ファイル名と保存先を設定する場合はこのような感じになります。
(パーミッション取得やチェックのロジックは割愛します)
fun onExportButtonClick(view: View) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "text/plain" putExtra(Intent.EXTRA_TITLE, getString(R.string.data_file_name)) } startActivityForResult(intent, resources.getInteger(R.integer.data_export_request)) }
「エクスポート」ボタンをタップするとこのメソッドに入ってきてファイル保存の画面が開きます。
いわゆる「名前を付けて保存」的な画面です。
上記の画面にて、ディレクトリやファイル名を設定して「保存」をタップするとプログラム的には「onActivityResult」が呼び出されます。
overrideしてファイルに書き出す処理を実装します。
こちらも詳細は割愛しますが、openOutputStreamメソッドでユーザーが指定したファイルに対して出力していく感じになります。
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { // エクスポート処理 if ((requestCode == resources.getInteger(R.integer.data_export_request)) && (resultCode == Activity.RESULT_OK)) { if (resultData?.data != null) { try { val uri = resultData.data as Uri val out = contentResolver.openOutputStream(uri) val writer = OutputStreamWriter(out) // これはDBからデータを読み込んでArrayListを作る処理。 // その結果がallRecordsに入ってくる。 getAllData() for (record in allRecords) { writer.write(record) writer.write("\n") } writer.close() } catch (e: FileNotFoundException) { e.printStackTrace() } catch (e: IOException) { e.printStackTrace() } } return } }
このような感じで指定した場所に指定した名前でファイルが出力されます。
まとめ
セキュリティやプライバシーに関しては頻繁にいろいろとアップデートされます。
アプリの実装も適宜アップデートしていく必要があることを痛感しました。
インポート機能(ファイル読込)についても同様の問題が発生します。
その修正についてはこちらの記事をご覧ください。
コメント