AndroidのSAF関連。
WhiteBoardCastで録画した動画をこれまではexternal storageに保存していたが、これはAndroid 10以降のスタイルでは無い。 という事で方針を考える。
MediaMixtureがFileを取る。だから録画中のファイルはapp specific directoryに入れておくのが良さそう。 最後に合成が終わったらMediaStoreに移動するのがいいだろうか?
スライドはpdfとしてexportするので、これはSAFを使うのがいいか? コードを見直すとpdfwriterのライブラリはOutputStreamでさえあれば良さそうなので、SAFで保存ファイルを選ばせる事は出来そう。
permissionとしてはAccess media files from shared storage - Android Developersの「Extra permissions needed for apps running on legacy devices」に、 Android 9以下ならREAD_EXTERNAL_STORAGEとWRITE_EXTERNAL_STORAGEがいるとの事。 Android 9はAPI Level 28。
RELATIVE_PATHは API Level 29から。
[SDK Platform release notes - Android Studio | Android Developers](https://developer.android.com/tools/releases/platforms) |
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
{
}
これまではファイルに書いてThumbnailUtils.createVideoThumbnail
を呼んでいたがMediaStoreのUriに書くようにしたので違うのを使う必要がある。
Access media files from shared storage - Android Developersの「Load file thumbnails」によると、loadThumbnailで良さそう。
と思ったらこれはAndroid Qから。Pより前は何使ったらいいんだ? とりあえずapp specific storageに保存されてる方のファイルにこれまで通りのThumbnailUtilsを使う方針にしてみよう。
Photo pickerが使いたいがFire Maxでは使え無さそう?要調査。
対応しているAndroidのインテント - Fireタブレット
GET_CONTENTと入れ替えActivityにするかなぁ。
Photo Pickerは無ければACTION_GET_CONTENTになって、しかも良い感じにやってくるメッセージは統一してくれるので、 Photo Pickerでいい気がしてきた。
そこにRecycleViewerでドラッグアンドロップで順番変えられる感じのスライドインポーター的なActivityを作るのが良さそう。
FireMaxでAudioRecordのreadがかえってこなくなるのはバッファサイズが大きすぎるらしい。minの倍ちょっとなんだが… という事で無事解決。
ContentDBのupdateがうまく行かなくなっている。以下のメッセージが出ている。
"Movement of content://media/external/video/media which isn't part of well-defined collection not allowed"
あおぞらAndroid教室でファイル周りの解説でも書こうかと思っていて、getExternalStoragePublicDirectoryを使おうと思ったらdeprecatedとなっているな。
Environment - Android Developers
だが同じ役割をする代替が無さそう。
getExternalStoragePublicDirectory deprecated in Android Q - Stack Overflow
RELATIVE_PATHが良さそうだが、これはAPI level 29から、だとか。さすがにこれはちょっと新しすぎるなぁ。
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "SomeFileName001")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/SomeDirName")
}
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
resolver.openOutputStream(uri).use {
// TODO something with the stream
}
Storage Access Frameworkの推奨シナリオを見ていたら、getExternalStoragePublicDirectoryを使えと書いてある… >Android storage use cases and best practices - Android Developers
正しくはバージョン見てgetExternalStoragePublicDirectoryと上記のコードを切り替えるんだろうが、さすがに違いすぎてかったるいな、これは。
まぁSAF使わずにDownloads下に保存したい、みたいなのはそれなりに雑なアプリな事が多いので、29以下を捨てる日が来るまではgetExternalStoragePublicDirectoryを使い続けるか。
こういうの互換にするためのandroidxでは無いのか?と思うが、使えそうなのが見当たらないな。
GoogleDriveなどで、ACTION_OPEN_DOCUMENTで得たuriをtakePersistableUriPermissionして保存し、デバイスを再起動してそのuriを開くと、以下のexception。
java.lang.SecurityException: Permission Denial: opening provider com.google.android.apps.docs.storagebackend.StorageBackendContentProvider from ProcessRecord{3833be7 4019:io.github.karino2.textdeck/u0a234} (pid=4019, uid=10234) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
ACTION_OPEN_DOCUMENTのintentに以下をやってもダメ。
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
直し方分からず。とりあえずSecurityExceptionをハンドルしておこう。
https://developer.android.com/training/data-storage/shared/documents-files#grant-access-directory
ここに書いてあるとおりにACTION_OPEN_DOCUMENT_TREEでIntentを投げて帰ってきたuriにContentResolverのqueryをやったら、UnsupportedOperationExceptionが。
なんで?とググってたらここに引っかかり、
SO: Unsupported Uri in ContentResolver when passing Uri returned by Intent
そこにかかれている謎のおまじない
val uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri))
を間に挟んだらたしかに読めるようになった。なんじゃこれ? そしてこれだとdirのエントリが帰ってきて中身が読めない。ふむ。
DocumentsContractというのがヒントっぽい、とドキュメントを読んで見る
https://developer.android.com/reference/android/provider/DocumentsContract
buildChildDocumentsUriUsingTree
がそれっぽいか?
androidx:DocumentFile がちゃんと動くっぽい!
java - DocumentFile is very slow - Stack Overflow
これは酷い。 コードはここか。
TreeDocumentFile.java - Android Code Search
Implement FastFile and use it instead of DocumentFile. · karino2/PngNote@b19dbf1
今後はこれを使っていこう。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
last_modified
0 = "document_id"
1 = "mime_type"
2 = "_display_name"
3 = "last_modified"
4 = "flags"
5 = "_size"
try{
val df = DocumentFile.fromTreeUri(this, treeUri)
val files = df?.listFiles()
// val uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri))
val uri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getDocumentId(treeUri))
val cursor = contentResolver.query(uri, null,
null, null, null, null) ?: return
val seqs = sequence {
cursor.use { cur ->
while (cur.moveToNext()) {
// last_modified
val disp = cur.getString(cur.getColumnIndex(OpenableColumns.DISPLAY_NAME))
val lindex = cur.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
val lm = cur.getLong(lindex)
val did = cur.getString(cur.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID))
yield("$disp : $lm: $did")
}
}
}
editText.setText(seqs.joinToString("\n"))
}catch (e: RuntimeException) {
editText.setText(e.message)
}
DocumentsContract.Document : Android Developers
isDirectoryは以下が呼ばれるのでMIME_TYPEで良さそう。
public static boolean isDirectory(Context context, Uri self) {
return DocumentsContract.Document.MIME_TYPE_DIR.equals(getRawType(context, self));
}
listFilesのコードを抜粋。
Cursor c = null;
try {
c = resolver.query(childrenUri, new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null);
while (c.moveToNext()) {
final String documentId = c.getString(0);
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(mUri,
documentId);
results.add(documentUri);
}
uriはbuildDocumentUriUsingTreeで作るらしい。