[Android] 004. assetsにあるファイルを外部アプリに渡す(provider)

assetsにあるテキストを外部アプリに渡したいと思い色々調べてましたが意外に正解の情報がなかったのでここでまとめます

コンテンツプロバイダ(ContentProvider)の実装

外部からファイルアクセスに指定するurlはfile://から始まるものではアクセスできずcontent://で始まるものでなければいけないようです

content://で始まるものにアクセスできるようにする仕組みがContentProviderで、これを実装してAndroidManifestに設定することで対応できます

今回はassetsのアクセス用に実装するのでopenAssetFile()のみ実装
onCreate()はfalseで返るようにしてquery()はとりはえずnullで返るようにしています

companion objectは対象ファイルのurlを指定する時に使います(参考先の実装そのままですがw

class AssetsFileProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        return false
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        return null
    }

    override fun getType(uri: Uri): String? {
        throw UnsupportedOperationException("Not yet implemented")
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        throw UnsupportedOperationException("Not yet implemented")
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        throw UnsupportedOperationException("Not yet implemented")
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        throw UnsupportedOperationException("Not yet implemented")
    }

    @Throws(FileNotFoundException::class)
    override fun openAssetFile(uri: Uri, mode: String): AssetFileDescriptor {
        try {
            return context!!.assets.openFd(uri.pathSegments.joinToString("/"))
        } catch (e: IOException) {
            e.printStackTrace()
            throw FileNotFoundException(e.message)
        }
    }

    companion object {
        // AndroidManifestで設定するproviderのauthoritiesに指定する値
        // 対象アプリのnamespaceにここでは.assetsを付加したものを指定しています
        private const val AUTHORITY = "com.example.yourapp.assets"
        val CONTENT_URI = Uri.parse("content://$AUTHORITY")!!
    }
}

AndroidManifestにproviderを設定

providerに指定する参照を許すpassの指定をresのxmlにします
ここではassets/sampleフォルダを指定しています
※ nameはなんでも良いです

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <asset name="sample" path="sample/" />
</paths>
<application>
    <provider
        android:name=".AssetsFileProvider"
        android:authorities="com.example.yourapp.assets"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepaths" />
    </provider>
</application>

Bundleのファイルの圧縮の抑止

ここではテキストとxmlを対象外にしています
※ xmlは存在していませんが複数指定の例です

android {
    aaptOptions {
        noCompress '.txt', '.xml'
    }
}

外部アプリにファイルを渡す(表示)

ここではテキストとxmlを対象外にしています
HTMLビューアなどで表示できます ※ ブラウザは表示できないです

val uri: Uri = AssetsFileProvider.CONTENT_URI.buildUpon()
    .appendPath("sample")
    .appendPath("Sample.txt")
    .build()

val intent = Intent().apply {
    action = Intent.ACTION_VIEW
    setDataAndType(uri, "text/plain")
    flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
val chooser = Intent.createChooser(intent, /* title */ null)
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()
) {
    // 戻ってきた時に処理が必要なら実装
}

launcher.launch(chooser)

外部アプリにファイルを渡す(コピー)

こちらは外部からassetsを参照しないのでproviderは不要です

val context = LocalContext.current
val launcher = 
    rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(mimeType = "text/plain")
    ) { uri ->
        val contentResolver = context.contentResolver
        val outputStream = uri?.let {
            contentResolver.openOutputStream(it)
        }

        context.assets.open("sample/sample.txt").use { inputStream ->
            outputStream?.let { inputStream.copyTo(it) }
        }
    }

launcher.launch("sample.txt")

Android Studio Flamingo 2022.2.1 Patch 2 built on May 12, 2023