LoginSignup
4
5

【Android] Jetpack Compose を使って OSSライセンス一覧を表示する

Last updated at Posted at 2024-01-21

はじめに

公式の Google Play 開発者サービス API である oss-licenses プラグインを使うと以下のような OSS ライセンス一覧を表示することができる。(詳細はここを参照。)
公式の OSS ライセンス表示

同プラグインは build.gradle(.kts)の dependencies セクションに追加したライブラリを収集してくれるのだが、oss-licenses プラグインの Activity 上に表示され、本体アプリとデザインの統一性がなくのでできるなら使用を避けたい。

これまでは、サードパーティー製のライブラリとしてクックパッドさんの License Tools Plugin for Android にお世話になっていたのだが残念ながらすでにアーカイブされている。他にこれといったサードパーティー製のライブラリが無いようなので、自分でコーディングすることにした。尚、UI には Jetpack Compose を用いる。

コーディングにあたっては「Google OSS License Gradle Pluginのデータをパースする」を参考にさせていただいた。同記事でも全然問題なく OSS ライセンス一覧を作れるのだが、途中 Okio ライブラリを使っているところがあって、その導入方法の記載がなく、戸惑う人がいるだろうというのと、なるべく Android 公式の API で実装したい人もいるだろう、加えて私のように UI も参考にしたいという人向けに本記事を書いた。

oss-licenses プラグインを導入

OSS ライセンス一覧のデータを作成すにるあたり、 Google Play 開発者サービスoss-licenses プラグインを使用する。

build.gradle.kts
buildscript {
  repositories {
    ...
    google()
  }
  dependencies {
    ...
    classpath("com.google.android.gms:oss-licenses-plugin:0.10.6")
  }
}
app/build.gradle.kts
plugins {
    id("com.android.application")
    id("com.google.android.gms.oss-licenses-plugin")
}

OSS ライセンス一覧のデータを作成

Android プロジェクトをビルドすると third_party_license_metadatathird_party_licenses が生成される。 debug ビルドと release ビルドでそれぞれ生成されるところも抑えておく。

スクリーンショット 2024-01-21 22.38.12.png

build/ にファイルが生成されるが、 わざわざ src/main/res/raw/ にコピーしなくても良い。この状態で src/main/res/raw/ に存在するファイル同様に R.raw.third_party_license_metadataR.raw.third_party_licenses としてソースコードからアクセスすることができる。

OSS ライセンス一覧のデータ

third_party_license_metadata ファイルと third_party_licenses ファイルの中身は例えば以下のようになっている。

third_party_license_metadata
0:46 Android Support Library Annotations
0:46 Android Support Library Custom View
0:46 Android Navigation Fragment
・・・
47:47 play-services-oss-licenses
95:1096 Animal Sniffer
・・・
third_party_licenses
http://www.apache.org/licenses/LICENSE-2.0.txt
https://developer.android.com/studio/terms.html
The MIT License

Copyright (c) 2008 Kohsuke Kawaguchi and codehaus.org.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
・・・

third_party_license_metadata ファイルには「offset」「length」「ライブラリ名」、third_party_licenses ファイルには「ライセンス条文」が入っている。

OSS ライセンス一覧のデータをパース

third_party_license_metadata ファイルの「offset」「length」を使えばthird_party_licenses ファイルからライセンス条文を取得できるようになっている。例えば、

third_party_license_metadata
95:1096 Animal Sniffer

は、 「Animal Sniffer」というライブラリのライセンス条文は third_party_licenses ファイルの offset= 95、length=1096 に格納されているという意味となる。
third_party_license_metadata ファイルとthird_party_licenses ファイルをパースし、 OSS ライセンス一覧データを生成するコードは以下となる。

LibraryLicenseList
import android.content.Context
import android.util.Log
import dev.seabat.android.usbdebugswitch.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader

data class LibraryLicenseList(val licenseList: List<LibraryLicense>) : List<LibraryLicense> by licenseList {
    companion object {
        suspend fun create(context: Context): LibraryLicenseList {
            val licenses = loadLibraries(context).map {
                LibraryLicense(it.name, loadLicense(context, it))
            }
            return LibraryLicenseList(licenses)
        }

        private suspend fun loadLibraries(context: Context): List<Library> {
            return withContext(Dispatchers.IO) {
                val inputSteam = context.resources.openRawResource(R.raw.third_party_license_metadata)
                inputSteam.use { inputSteam ->
                    val reader = BufferedReader(InputStreamReader(inputSteam, "UTF-8"))
                    reader.use { bufferedReader ->
                        val libraries = mutableListOf<Library>()
                        while (true) {
                            val line = bufferedReader.readLine() ?: break
                            val (position, name) = line.split(' ', limit = 2)
                            val (offset, length) = position.split(':').map { it.toInt() }
                            libraries.add(Library(name, offset, length))
                        }
                        libraries.toList()
                    }
                }
            }
        }

        private suspend fun loadLicense(context: Context, library: Library): String {
            Log.d("LicenseList", "${library.name} ${library.offset} ${library.length}")
            return withContext(Dispatchers.IO) {
                val charArray = CharArray(library.length)
                val inputStream = context.resources.openRawResource(R.raw.third_party_licenses)
                inputStream.use { stream ->
                    val bufferedReader = BufferedReader(InputStreamReader(stream, "UTF-8"))
                    bufferedReader.use { reader ->
                        reader.skip(library.offset.toLong())
                        reader.read(charArray, 0, library.length)
                    }
                }
                String(charArray)
            }
        }
    }
}

data class Library(
    val name: String,
    val offset: Int,
    val length: Int,
)

data class LibraryLicense(
    val name: String,
    val terms: String //条文
)

LibraryLicenseList#create はファイル I/O を伴う少し重い処理のため、非同期で実行されるよう suspend メソッドとなっている。よって、 以下のように Coroutine ブロックから呼びだす。

LicensesViewModel.kt
class LicenseFragment : Fragment() {
    private val _licensesStateFlow = MutableStateFlow(LibraryLicenseList(arrayListOf()))
    private val licensesStateFlow = _licensesStateFlow.asStateFlow()
・・・    
    fun createLicenses() {
        lifecycleScope.launch {
            _licensesStateFlow.update {
                LibraryLicenseList.create(requireContext())
            }
        }
    }
・・・

コンポーザブル関数でOSSライセンス一覧を表示する

ここまででOSS ライセンス一覧のデータを StateFlow<LibraryLicenseList> オブジェクトとして生成できているのであとはコンポーザブル関数で UI 表示だけである。

LicenseContent.kt
@Composable
fun LicenseContent(
    modifier: Modifier = Modifier,
    licensesStateFlow: StateFlow<LibraryLicenseList>
) {
    val licensesState by licensesStateFlow.collectAsState()
    LazyColumn(modifier = modifier) {
        items(licensesState) {
            Column() {
                Text(text = it.name, style = MaterialTheme.typography.titleMedium)
                Spacer(modifier = Modifier.height(8.dp))
                Text(text = it.terms, fontSize = 8.sp)
                Spacer(modifier = Modifier.height(24.dp))
            }
        }
    }
}

スクリーンショット 2024-01-21 23.27.18.png

補足

oss-licenses プラグインを導入すると、同プラグインの AndroidManifest.xml が本体アプリの AndroidManifest.xml にマージされる。別に悪さはしないだろうが後で存在を知って驚かなよう覚えておくよい。

        <activity
            android:label="@ref/0x7f0f006e"
            android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" />

        <activity
            android:name="com.google.android.gms.oss.licenses.OssLicensesActivity" />

        <activity
            android:theme="@ref/0x01030010"
            android:name="com.google.android.gms.common.api.GoogleApiActivity"
            android:exported="false" />

参考

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5