どうもこんばんわ。
超会議とても良かったです。1押す
ちなみに私は超パーティー再放送とアニメ一挙放送を主に見てました。Vはわからんのでな。
らららコッペパンってらき☆すただったんだ。
今年のGoogle IOが中止になったどころかAndroid 11に関してもドキュメントが全然更新されなくなっちゃって大丈夫なんかこれ。
追記 2020/04/28:そういえばMediaStoreに関しては一切触れてなかった。触れる機会があれば書くかも。
ほんぺん
ところでAndroid10から自由にフォルダにアクセスできなくなりました。これにより自由にDownloadフォルダ
とかPicturesフォルダ
等へアクセスしたりファイル作成とかができなくなりました。
じゃあどこに保存すればいいんだって話ですが、Scoped Storage(日本語:対象範囲別外部ストレージ)
という仕組みが作られ、アプリごとに権限無しで書き込むことができるフォルダが作られるようになりました。
ドキュメント:https://developer.android.com/training/data-storage/files/external-scoped?hl=ja
環境
あたい |
なまえ |
端末 |
Pixel 3 XL |
Android |
11 Developer Preview 2 |
言語 |
Kotlin |
SDCard |
知らんわ(Pixelに刺さらないのでわからない。) |
Android 11 から(保存ではない)
Downloadフォルダ
とかPicturesフォルダ
等に読み取り専用でならアクセスできるようになったっぽい?
1 2 3
| File("/storage/emulated/0/").listFiles()?.forEach { println(it.name) }
|
↓実行結果
1 2 3 4 5 6 7 8 9 10
| I/System.out: Android I/System.out: Music I/System.out: Podcasts I/System.out: Ringtones I/System.out: Alarms I/System.out: Notifications I/System.out: Pictures I/System.out: Movies I/System.out: Download I/System.out: DCIM
|
一番いい?→Intent.ACTION_OPEN_DOCUMENT_TREE
#これはなに?
自由にはアクセスできない代わりにユーザーが指定したフォルダにはアクセスできるよってやつ。
写真アプリなんだけどScoped Storageに保存するのではなくPicturesフォルダに保存したいんだって時に使う。
これを使うと?
内部ストレージを指定するとDownloadフォルダ
とかPicturesフォルダ
にアクセスできるちょっとやばめ。
一度許可を貰えればこっちのもんです。
実装
ライブラリ入れる
appフォルダにある方のbuild.gradle
です。
1 2 3 4
| implementation "androidx.preference:preference:1.1.0"
implementation "androidx.documentfile:documentfile:1.0.1"
|
あと楽するためにJavaのバージョンを8にします。
これとこれってなってるところね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| android { compileSdkVersion 29 buildToolsVersion "29.0.3"
defaultConfig { applicationId "io.github.takusan23.actionopendocumenttreesample" minSdkVersion 21 targetSdkVersion 29 versionCode 1 versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" }
buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { targetCompatibility 1.8 sourceCompatibility 1.8 } kotlinOptions { jvmTarget = '1.8' } }
|
レイアウト
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity">
<EditText android:id="@+id/editText" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:ems="10" android:inputType="textPersonName" android:text="" />
<Button android:id="@+id/path_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="保存先指定" />
<Button android:id="@+id/save_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="保存" />
<Button android:id="@+id/read_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="読み込み" /> </LinearLayout>
|
許可をもらう
takePersistableUriPermission()が大事?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| val REQUEST_CODE = 816 lateinit var prefSetting: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) prefSetting = PreferenceManager.getDefaultSharedPreferences(this) path_button.setOnClickListener { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(intent, REQUEST_CODE) }
} override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) { val uri = data?.data ?: return contentResolver.takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) prefSetting.edit { putString("uri", uri.toString()) } } }
|
これで実行して「保存先指定」ボタンを押して保存先を選びます。
許可します
保存、読み込みを実装する
最終的のMainActivity.kt
はこうです!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| class MainActivity : AppCompatActivity() {
val REQUEST_CODE = 816 lateinit var prefSetting: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main)
prefSetting = PreferenceManager.getDefaultSharedPreferences(this)
path_button.setOnClickListener { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(intent, REQUEST_CODE) }
save_button.setOnClickListener { saveFile() }
read_button.setOnClickListener { readFile() }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) { val uri = data?.data ?: return contentResolver.takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) prefSetting.edit { putString("uri", uri.toString()) } } }
private fun saveFile() { val uri = prefSetting.getString("uri", "")?.toUri() ?: return DocumentFile.fromTreeUri(this, uri)?.apply { val textFile = if (findFile("test.txt")?.exists() == true) { findFile("test.txt") ?: return@apply } else { createFile("text/plain", "test.txt") ?: return@apply } contentResolver.openOutputStream(textFile.uri)?.apply { write(editText.text?.toString()?.toByteArray()) close() } } }
private fun readFile() { val uri = prefSetting.getString("uri", "")?.toUri() ?: return DocumentFile.fromTreeUri(this, uri)?.apply { if (findFile("test.txt")?.exists() == false) { return@apply } val textFile = findFile("test.txt") ?: return@apply val text = contentResolver.openInputStream(textFile.uri)?.bufferedReader()?.readLine() editText.setText(text) } }
}
|
これで保存、読み込みができるようになりました。やったー
これで内部ストレージのパスを指定すると・・?
スクリーンショットのフォルダにもアクセスできます。
1 2 3 4 5 6 7
| val uri = prefSetting.getString("uri", "")?.toUri() ?: return
DocumentFile.fromTreeUri(this, uri)?.apply { findFile("Pictures")?.findFile("Screenshots")?.listFiles()?.forEach { println(it.name) } }
|
Android 11 DP2 の仕様?
SDカードに保存する手段だったらしいけどAndroid 11からこの方法でSDカードの場所を指定するのはできなくなるらしいぞ。
一応ソースコード置いておきますね↓
https://github.com/takusan23/ActionOpenDocumentTreeSample
Scoped Storageで保存
Scoped Storageのパスは以下の関数で取れます。(キャッシュ系は省くぜ)
この2つはScoped Storageなフォルダのパスなので権限無しでFileクラスで読み書きできます。(Kotlinだと拡張関数で幸せになれる。)
getExternalFilesDir(null)?.path
/storage/emulated/0/Android/data/io.github.takusan23.scopedstoragesample/files
- データはアンインストール時に削除されます。
externalMediaDirs[0].path
/storage/emulated/0/Android/media/io.github.takusan23.scopedstoragesample
- データはアンインストール時に削除されます。
- MediaStoreのスキャンに対応してるので他のアプリでも利用できる?
Googleフォトの新しいフォルダ見つけたよ!バックアップする?のやつに認知されるのはこれだっけ。
- Android 11 DP2で検証できなかった。ごめん。
- Android R(11)から非推奨になりました。でも使えるっぽい。
余計なことするなよ
両者共にファイルパスが長い/アンインストール時にデータを消すのが特徴。
以下書き込みサンプルです。
Kotlinの拡張関数(writeText()とか)でらくらくテキスト保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity">
<EditText android:id="@+id/editText" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:ems="10" android:inputType="textPersonName" android:text="" />
<Button android:id="@+id/save_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="保存" />
<Button android:id="@+id/read_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="読み込み" /> </LinearLayout>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) save_button.setOnClickListener { saveFile() }
read_button.setOnClickListener { readFile() }
}
private fun saveFile() { File("${externalMediaDirs[0].path}/test.txt").apply { createNewFile() writeText(editText.text.toString()) } }
private fun readFile() { File("${externalMediaDirs[0].path}/test.txt").apply { if (exists()) { val text = readText() editText.setText(text) } } }
}
|
Android 11 DP2 の仕様?
ファイルマネージャー(Filesアプリとか)から見れなくなった。
USB接続してパソコンで見るかAndroid StudioのDevice Explorerを使うしかない?
ちなみに
ドキュメントとか関数名とかにExternal(日本語:外部)っていう文字列があると外部ストレージのSDカードのことだと思っちゃうけど実は違って、
アプリ自身とroot権限のある環境?でしかアクセスできないパスを返す関数getFilesDir()
の場所のことを内部ストレージとしているらしくて、
それ以外が外部扱いということでExternalって文字列が使われているんだって。
わからんわ。
Storage Access Framework で保存
好きな場所に保存したいときはこれを使えって
ファイル選択画面みたいなUIでファイルの保存先を選んで保存したあと、onActivityResult
でUriを受け取りopenOutputStream
を使って書き込むらしいです。
以下コード
レイアウトは上のScoped Storageを使い回す。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class MainActivity : AppCompatActivity() { val REQUEST_CODE = 810
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main)
save_button.setOnClickListener { openSAF() }
}
private fun openSAF() { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { type = "text/*" putExtra(Intent.EXTRA_TITLE, "test.txt") } startActivityForResult(intent, REQUEST_CODE) }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) { val uri = data?.data ?: return contentResolver.openOutputStream(uri)?.apply { write(editText.text.toString().toByteArray()) close() } } }
}
|
これ使えばDownloadフォルダ
だろうと自由に保存できます。保存するときにいちいち保存先を選ぶ必要があって使いにくいけど。
これが保存画面で、保存先を選ぶ。
ファイルマネージャーで見るとちゃんと作成されていることがわかる。
め!ん!ど!い!