StorageAccessFramework - RandomThoughts
RandomThoughts

RandomThoughts

StorageAccessFramework

Contents:
  1. メディアファイルの周辺
  2. 全体の方針
  3. permission
  4. RELATIVE_PATH
  5. サムネイル
  6. TODO
  7. getExternalStoragePublicDirectory周辺
  8. しばらくしたあとのアクセスでSecurityExceptionが上がる
  9. 試行錯誤した時のメモ
  10. DocumentFileはlistFilesとかnameとかがいちいち遅い
  11. DocumentFileと同じような機能を持つFastFileを実装
  12. 雑多なメモ

AndroidのSAF関連。


メディアファイルの周辺

WhiteBoardCastで録画した動画をこれまではexternal storageに保存していたが、これはAndroid 10以降のスタイルでは無い。 という事で方針を考える。

全体の方針

MediaMixtureがFileを取る。だから録画中のファイルはapp specific directoryに入れておくのが良さそう。 最後に合成が終わったらMediaStoreに移動するのがいいだろうか?

スライドはpdfとしてexportするので、これはSAFを使うのがいいか? コードを見直すとpdfwriterのライブラリはOutputStreamでさえあれば良さそうなので、SAFで保存ファイルを選ばせる事は出来そう。

permission

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

RELATIVE_PATHは API Level 29から。

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を使う方針にしてみよう。

TODO

  • DONE … WorkDirの下のmp4を全て削除
  • PDFのexportをSAFに
  • DONE … drag indicatorのアイコンを組み込む
  • DONE … 横画面でのスライド選びをなんとかしたい。
  • DONE … MultiGalleryが機能してないので方針を考える

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"

  • where句でidと一致するのをupdateしていたが、urlを直接指定するように変更

getExternalStoragePublicDirectory周辺

あおぞら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では無いのか?と思うが、使えそうなのが見当たらないな。

しばらくしたあとのアクセスでSecurityExceptionが上がる

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 がちゃんと動くっぽい!

DocumentFileはlistFilesとかnameとかがいちいち遅い

java - DocumentFile is very slow - Stack Overflow

これは酷い。 コードはここか。

TreeDocumentFile.java - Android Code Search

DocumentFileと同じような機能を持つFastFileを実装

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で作るらしい。