はじめに
公式の Google Play 開発者サービス API である oss-licenses
プラグインを使うと以下のような 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
プラグインを使用する。
buildscript {
repositories {
...
google()
}
dependencies {
...
classpath("com.google.android.gms:oss-licenses-plugin:0.10.6")
}
}
plugins {
id("com.android.application")
id("com.google.android.gms.oss-licenses-plugin")
}
OSS ライセンス一覧のデータを作成
Android プロジェクトをビルドすると third_party_license_metadata
と third_party_licenses
が生成される。 debug ビルドと release ビルドでそれぞれ生成されるところも抑えておく。
build/ にファイルが生成されるが、 わざわざ src/main/res/raw/ にコピーしなくても良い。この状態で src/main/res/raw/ に存在するファイル同様に R.raw.third_party_license_metadata
、R.raw.third_party_licenses
としてソースコードからアクセスすることができる。
OSS ライセンス一覧のデータ
third_party_license_metadata
ファイルと third_party_licenses
ファイルの中身は例えば以下のようになっている。
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
・・・
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
ファイルからライセンス条文を取得できるようになっている。例えば、
95:1096 Animal Sniffer
は、 「Animal Sniffer」というライブラリのライセンス条文は third_party_licenses
ファイルの offset= 95、length=1096 に格納されているという意味となる。
third_party_license_metadata
ファイルとthird_party_licenses
ファイルをパースし、 OSS ライセンス一覧データを生成するコードは以下となる。
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 ブロックから呼びだす。
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 表示だけである。
@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))
}
}
}
}
補足
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" />
参考