From 1a90b4b53e9ebf9e27f91790e0b07556f5d59842 Mon Sep 17 00:00:00 2001 From: Xavier Gouchet Date: Tue, 26 Jan 2021 10:44:24 +0100 Subject: [PATCH] Cleanup the OSS generation and display --- .../core/utils/DocumentExtension.kt | 7 +- .../core/utils/ManifestUtils.kt | 2 +- .../packageexplorer/details/DetailsSource.kt | 76 +++++++----- .../details/adapter/AppInfoViewHolder.kt | 8 +- .../details/adapter/AppInfoViewModel.kt | 23 ++-- .../oss/OSSDependenciesSource.kt | 72 +++++++---- .../packageexplorer/oss/OSSDependency.kt | 1 + .../xgouchet/gradle/plugin/OSSDependency.kt | 19 +++ .../gradle/plugin/OSSLicenseProvider.kt | 117 ++++++++++-------- 9 files changed, 191 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/fr/xgouchet/packageexplorer/core/utils/DocumentExtension.kt b/app/src/main/java/fr/xgouchet/packageexplorer/core/utils/DocumentExtension.kt index 75d2116..eda34cb 100644 --- a/app/src/main/java/fr/xgouchet/packageexplorer/core/utils/DocumentExtension.kt +++ b/app/src/main/java/fr/xgouchet/packageexplorer/core/utils/DocumentExtension.kt @@ -10,14 +10,12 @@ import org.w3c.dom.NodeList * Created by Ekrem HATİPOĞLU on 11.10.2020. */ - object ManifestType { const val APPLICATION = "application" const val INTENT_FILTER = "intent-filter" const val ATTRS = "attrs" } - data class AndroidManifest(val items: List) data class Item(val tagName: String, val attrs: Map, val childList: List = listOf()) @@ -42,7 +40,7 @@ fun List.formatItem(): List>> { forEach { intentFilter -> val item = mutableMapOf>() - if (intentFilter.attrs.isNotEmpty()){ + if (intentFilter.attrs.isNotEmpty()) { item[ATTRS] = intentFilter.attrs } @@ -55,7 +53,6 @@ fun List.formatItem(): List>> { return list } - fun Item.getItemsFromChildByType(type: String): List { return childList.filter { it.tagName == type } } @@ -101,5 +98,3 @@ private fun NodeList.takeFirst(block: Node.() -> Unit) { if (this.length > 0) this.item(0).block() } - - diff --git a/app/src/main/java/fr/xgouchet/packageexplorer/core/utils/ManifestUtils.kt b/app/src/main/java/fr/xgouchet/packageexplorer/core/utils/ManifestUtils.kt index dc2393a..c1821b8 100644 --- a/app/src/main/java/fr/xgouchet/packageexplorer/core/utils/ManifestUtils.kt +++ b/app/src/main/java/fr/xgouchet/packageexplorer/core/utils/ManifestUtils.kt @@ -31,7 +31,7 @@ fun exportManifestFromPackage( } fun exportManifestDomFromPackage( - info: PackageInfo + info: PackageInfo ): Document { return parseManifestFile(getPackageApk(info)) } diff --git a/app/src/main/java/fr/xgouchet/packageexplorer/details/DetailsSource.kt b/app/src/main/java/fr/xgouchet/packageexplorer/details/DetailsSource.kt index d3d21a1..79fd84f 100644 --- a/app/src/main/java/fr/xgouchet/packageexplorer/details/DetailsSource.kt +++ b/app/src/main/java/fr/xgouchet/packageexplorer/details/DetailsSource.kt @@ -2,20 +2,38 @@ package fr.xgouchet.packageexplorer.details import android.content.ComponentName import android.content.Context -import android.content.pm.* +import android.content.pm.ApplicationInfo +import android.content.pm.FeatureInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.Signature import android.graphics.drawable.Drawable import android.os.Build import fr.xgouchet.packageexplorer.R -import fr.xgouchet.packageexplorer.core.utils.* -import fr.xgouchet.packageexplorer.details.adapter.* +import fr.xgouchet.packageexplorer.core.utils.AndroidManifest +import fr.xgouchet.packageexplorer.core.utils.ManifestType +import fr.xgouchet.packageexplorer.core.utils.filterByName +import fr.xgouchet.packageexplorer.core.utils.formatItem +import fr.xgouchet.packageexplorer.core.utils.getItemsFromChildByType +import fr.xgouchet.packageexplorer.core.utils.humanReadableName +import fr.xgouchet.packageexplorer.details.adapter.AppInfoBullet +import fr.xgouchet.packageexplorer.details.adapter.AppInfoHeader +import fr.xgouchet.packageexplorer.details.adapter.AppInfoSimple +import fr.xgouchet.packageexplorer.details.adapter.AppInfoSubHeader +import fr.xgouchet.packageexplorer.details.adapter.AppInfoType +import fr.xgouchet.packageexplorer.details.adapter.AppInfoViewModel +import fr.xgouchet.packageexplorer.details.adapter.AppInfoWithIcon +import fr.xgouchet.packageexplorer.details.adapter.AppInfoWithSubtitle +import fr.xgouchet.packageexplorer.details.adapter.AppInfoWithSubtitleAndAction +import fr.xgouchet.packageexplorer.details.adapter.AppInfoWithSubtitleAndIcon import io.reactivex.ObservableEmitter -import timber.log.Timber import java.io.File import java.security.MessageDigest import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import javax.security.cert.CertificateException import javax.security.cert.X509Certificate +import timber.log.Timber open class DetailsSource(val context: Context) { @@ -30,10 +48,10 @@ open class DetailsSource(val context: Context) { protected var androidManifestXml: AndroidManifest? = null protected fun extractMainInfo( - emitter: ObservableEmitter, - packageInfo: PackageInfo, - applicationInfo: ApplicationInfo?, - apkFile: File? + emitter: ObservableEmitter, + packageInfo: PackageInfo, + applicationInfo: ApplicationInfo?, + apkFile: File? ) { emitter.apply { onNext(AppInfoWithSubtitle(AppInfoType.INFO_TYPE_METADATA, PACKAGE_NAME, packageInfo.packageName)) @@ -92,9 +110,9 @@ open class DetailsSource(val context: Context) { } protected fun extractActivities( - emitter: ObservableEmitter, - packageInfo: PackageInfo, - packageManager: PackageManager + emitter: ObservableEmitter, + packageInfo: PackageInfo, + packageManager: PackageManager ) { val activities = packageInfo.activities ?: return val packageName = packageInfo.packageName @@ -115,14 +133,13 @@ open class DetailsSource(val context: Context) { onNext(AppInfoWithSubtitleAndIcon(AppInfoType.INFO_TYPE_ACTIVITIES, label, name, activity.name, icon)) extractIntentFilters(this, AppInfoType.INFO_TYPE_ACTIVITIES, activity.name) - } } } protected fun extractServices( - emitter: ObservableEmitter, - packageInfo: PackageInfo + emitter: ObservableEmitter, + packageInfo: PackageInfo ) { val services = packageInfo.services ?: return val packageName = packageInfo.packageName @@ -135,14 +152,13 @@ open class DetailsSource(val context: Context) { onNext(AppInfoSimple(AppInfoType.INFO_TYPE_SERVICES, simplifiedName, service.name)) extractIntentFilters(this, AppInfoType.INFO_TYPE_SERVICES, service.name) - } } } protected fun extractProviders( - emitter: ObservableEmitter, - packageInfo: PackageInfo + emitter: ObservableEmitter, + packageInfo: PackageInfo ) { val providers = packageInfo.providers ?: return val packageName = packageInfo.packageName @@ -158,8 +174,8 @@ open class DetailsSource(val context: Context) { } protected fun extractReceivers( - emitter: ObservableEmitter, - packageInfo: PackageInfo + emitter: ObservableEmitter, + packageInfo: PackageInfo ) { val receivers = packageInfo.receivers ?: return val packageName = packageInfo.packageName @@ -177,8 +193,8 @@ open class DetailsSource(val context: Context) { } protected fun extractCustomPermissions( - emitter: ObservableEmitter, - packageInfo: PackageInfo + emitter: ObservableEmitter, + packageInfo: PackageInfo ) { val permissions = packageInfo.permissions ?: return @@ -201,8 +217,8 @@ open class DetailsSource(val context: Context) { } protected fun extractPermissions( - emitter: ObservableEmitter, - packageInfo: PackageInfo + emitter: ObservableEmitter, + packageInfo: PackageInfo ) { val permissions = packageInfo.requestedPermissions ?: return val customPermissions = packageInfo.permissions?.toList()?.map { it.name } ?: emptyList() @@ -239,8 +255,8 @@ open class DetailsSource(val context: Context) { } protected fun extractFeatures( - emitter: ObservableEmitter, - packageInfo: PackageInfo + emitter: ObservableEmitter, + packageInfo: PackageInfo ) { val features = packageInfo.reqFeatures ?: return @@ -272,8 +288,8 @@ open class DetailsSource(val context: Context) { } protected fun extractSignatures( - emitter: ObservableEmitter, - packageInfo: PackageInfo + emitter: ObservableEmitter, + packageInfo: PackageInfo ) { val signatures: Array = packageInfo.signatures ?: return if (signatures.isEmpty()) return @@ -301,7 +317,7 @@ open class DetailsSource(val context: Context) { private fun extractIntentFilters(observableEmitter: ObservableEmitter, infoType: Int, name: String) { androidManifestXml?.let { manifest -> val parent = manifest.filterByName(name) - val intentFilters= parent.getItemsFromChildByType(ManifestType.INTENT_FILTER) + val intentFilters = parent.getItemsFromChildByType(ManifestType.INTENT_FILTER) intentFilters.formatItem().forEach { intentFilter -> observableEmitter.onNext(AppInfoSubHeader(infoType, ManifestType.INTENT_FILTER)) @@ -363,8 +379,4 @@ open class DetailsSource(val context: Context) { "${HEX_CHARS[firstIndex]}${HEX_CHARS[secondIndex]}" } } - } - - - diff --git a/app/src/main/java/fr/xgouchet/packageexplorer/details/adapter/AppInfoViewHolder.kt b/app/src/main/java/fr/xgouchet/packageexplorer/details/adapter/AppInfoViewHolder.kt index 124e2f5..bdbb2de 100644 --- a/app/src/main/java/fr/xgouchet/packageexplorer/details/adapter/AppInfoViewHolder.kt +++ b/app/src/main/java/fr/xgouchet/packageexplorer/details/adapter/AppInfoViewHolder.kt @@ -52,8 +52,8 @@ class AppInfoHeaderViewHolder( } class AppInfoSubHeaderViewHolder( - itemView: View, - listener: BiConsumer? + itemView: View, + listener: BiConsumer? ) : AppInfoViewHolder(itemView, listener) { private val titleView: TextView = itemView.findViewById(R.id.title) @@ -76,8 +76,8 @@ class AppInfoSimpleViewHolder( } class AppInfoBulletViewHolder( - itemView: View, - listener: BiConsumer? + itemView: View, + listener: BiConsumer? ) : AppInfoViewHolder(itemView, listener) { private val iconView: ImageView = itemView.findViewById(R.id.icon) diff --git a/app/src/main/java/fr/xgouchet/packageexplorer/details/adapter/AppInfoViewModel.kt b/app/src/main/java/fr/xgouchet/packageexplorer/details/adapter/AppInfoViewModel.kt index d0b8b29..e66fe8f 100644 --- a/app/src/main/java/fr/xgouchet/packageexplorer/details/adapter/AppInfoViewModel.kt +++ b/app/src/main/java/fr/xgouchet/packageexplorer/details/adapter/AppInfoViewModel.kt @@ -44,15 +44,14 @@ interface AppInfoSelectable { } data class AppInfoSubHeader( - val type: Int, - val header: String + val type: Int, + val header: String ) : AppInfoViewModel(type, "SubHeader {$type} “$header”") - data class AppInfoSimple( - val type: Int, - val title: String, - val raw: String? = null + val type: Int, + val title: String, + val raw: String? = null ) : AppInfoViewModel(type, "Simple {$type} “$title”"), AppInfoSelectable { @@ -61,12 +60,12 @@ data class AppInfoSimple( } data class AppInfoBullet( - val type: Int, - val name: String, - val value: String, - val raw: String? = null, - val separator: String = "=", - @DrawableRes val icon: Int = R.drawable.ic_bullet + val type: Int, + val name: String, + val value: String, + val raw: String? = null, + val separator: String = "=", + @DrawableRes val icon: Int = R.drawable.ic_bullet ) : AppInfoViewModel(type, "Bullet {$type} “$value”"), diff --git a/app/src/main/java/fr/xgouchet/packageexplorer/oss/OSSDependenciesSource.kt b/app/src/main/java/fr/xgouchet/packageexplorer/oss/OSSDependenciesSource.kt index f94b034..3f4f78f 100644 --- a/app/src/main/java/fr/xgouchet/packageexplorer/oss/OSSDependenciesSource.kt +++ b/app/src/main/java/fr/xgouchet/packageexplorer/oss/OSSDependenciesSource.kt @@ -14,7 +14,7 @@ import io.reactivex.ObservableOnSubscribe import java.io.IOException class OSSDependenciesSource(val context: Context) : - ObservableOnSubscribe { + ObservableOnSubscribe { override fun subscribe(emitter: ObservableEmitter) { @@ -23,29 +23,61 @@ class OSSDependenciesSource(val context: Context) : val gson: Gson = GsonBuilder().create() val data = gson.fromJson( - stream.reader(Charsets.UTF_8), - Array::class.java + stream.reader(Charsets.UTF_8), + Array::class.java ).sortedBy { it.identifier } val androidX = data.filter { it.identifier.startsWith("androidx") } val kotlin = data.filter { it.identifier.startsWith("org.jetbrains.kotlin") } val misc = data.filter { (!it.identifier.startsWith("org.jetbrains.kotlin")) && - (!it.identifier.startsWith("androidx")) + (!it.identifier.startsWith("androidx")) } if (androidX.isNotEmpty()) { - emitter.onNext(AppInfoHeader(AppInfoType.INFO_TYPE_ANDROID, "AndroidX", R.drawable.ic_oss_android_logo)) - androidX.forEach { emitter.onNext(convertDependency(it, AppInfoType.INFO_TYPE_ANDROID)) } + emitter.onNext( + AppInfoHeader( + AppInfoType.INFO_TYPE_ANDROID, + "AndroidX", + R.drawable.ic_oss_android_logo + ) + ) + androidX.forEach { + emitter.onNext( + convertDependency( + it, + AppInfoType.INFO_TYPE_ANDROID + ) + ) + } } if (kotlin.isNotEmpty()) { - emitter.onNext(AppInfoHeader(AppInfoType.INFO_TYPE_KOTLIN, "Kotlin", R.drawable.ic_oss_kotlin_logo)) - kotlin.forEach { emitter.onNext(convertDependency(it, AppInfoType.INFO_TYPE_KOTLIN)) } + emitter.onNext( + AppInfoHeader( + AppInfoType.INFO_TYPE_KOTLIN, + "Kotlin", + R.drawable.ic_oss_kotlin_logo + ) + ) + kotlin.forEach { + emitter.onNext( + convertDependency( + it, + AppInfoType.INFO_TYPE_KOTLIN + ) + ) + } } if (misc.isNotEmpty()) { - emitter.onNext(AppInfoHeader(AppInfoType.INFO_TYPE_MISC, "Misc", R.drawable.ic_oss_package_logo)) + emitter.onNext( + AppInfoHeader( + AppInfoType.INFO_TYPE_MISC, + "Misc", + R.drawable.ic_oss_package_logo + ) + ) misc.forEach { emitter.onNext(convertDependency(it, AppInfoType.INFO_TYPE_MISC)) } } @@ -55,22 +87,14 @@ class OSSDependenciesSource(val context: Context) : } private fun convertDependency(ossDependency: OSSDependency, type: Int): AppInfoViewModel { - return if (ossDependency.sourceUrl.isNullOrBlank()) { - AppInfoWithSubtitle( - type, - ossDependency.name, - "${ossDependency.identifier}\n${ossDependency.license}", - ossDependency.identifier - ) + val title = "${ossDependency.name} — ${ossDependency.licenseKey}" + val subtitle = "${ossDependency.identifier}\n${ossDependency.license}" + val raw = ossDependency.identifier + val actionData = ossDependency.sourceUrl + return if (actionData.isNullOrBlank()) { + AppInfoWithSubtitle(type, title, subtitle, raw) } else { - AppInfoWithSubtitleAndAction( - type, - ossDependency.name, - "${ossDependency.identifier}\n${ossDependency.license}", - ossDependency.identifier, - "Source", - ossDependency.sourceUrl - ) + AppInfoWithSubtitleAndAction(type, title, subtitle, raw, "Source", actionData) } } } diff --git a/app/src/main/java/fr/xgouchet/packageexplorer/oss/OSSDependency.kt b/app/src/main/java/fr/xgouchet/packageexplorer/oss/OSSDependency.kt index 1e077fc..8a504af 100644 --- a/app/src/main/java/fr/xgouchet/packageexplorer/oss/OSSDependency.kt +++ b/app/src/main/java/fr/xgouchet/packageexplorer/oss/OSSDependency.kt @@ -6,5 +6,6 @@ data class OSSDependency( @SerializedName("name") val name: String, @SerializedName("identifier") val identifier: String, @SerializedName("license") val license: String, + @SerializedName("licenseKey") val licenseKey: String, @SerializedName("sourceUrl") val sourceUrl: String? ) diff --git a/buildSrc/src/main/kotlin/fr/xgouchet/gradle/plugin/OSSDependency.kt b/buildSrc/src/main/kotlin/fr/xgouchet/gradle/plugin/OSSDependency.kt index 97d0b9a..4de91b0 100644 --- a/buildSrc/src/main/kotlin/fr/xgouchet/gradle/plugin/OSSDependency.kt +++ b/buildSrc/src/main/kotlin/fr/xgouchet/gradle/plugin/OSSDependency.kt @@ -6,6 +6,15 @@ data class OSSDependency( val license: String, val sourceUrl: String? ) { + + val licenseKey = SHORT_LICENSE_NAME[license] + + init { + if (licenseKey == null) { + System.err.println("Unknwon license: $license for $name [$identifier]") + } + } + fun toJson(): String { val sourceField = if (sourceUrl != null) { "\"sourceUrl\":\"${sourceUrl}\"" @@ -16,7 +25,17 @@ data class OSSDependency( "\"name\":\"$name\", " + "\"identifier\":\"$identifier\", " + "\"license\":\"$license\"," + + "\"licenseKey\":\"$licenseKey\"," + sourceField + "}" } + + companion object { + private val SHORT_LICENSE_NAME = mapOf( + "The Apache Software License, Version 2.0" to "Apache-2.0", + "The Apache License, Version 2.0" to "Apache-2.0", + "MIT License" to "MIT", + "CC0" to "CC0 1.0" + ) + } } diff --git a/buildSrc/src/main/kotlin/fr/xgouchet/gradle/plugin/OSSLicenseProvider.kt b/buildSrc/src/main/kotlin/fr/xgouchet/gradle/plugin/OSSLicenseProvider.kt index dd80fc6..c758b88 100644 --- a/buildSrc/src/main/kotlin/fr/xgouchet/gradle/plugin/OSSLicenseProvider.kt +++ b/buildSrc/src/main/kotlin/fr/xgouchet/gradle/plugin/OSSLicenseProvider.kt @@ -17,8 +17,8 @@ class OSSLicenseProvider { val pomFilesList = resolvePomFiles(project, dependencies) return listOSSDependencies(dependencies, pomFilesList) - .toSet() - .sortedBy { it.identifier } + .toSet() + .sortedBy { it.identifier } } // region Internal @@ -27,15 +27,15 @@ class OSSLicenseProvider { project: Project ): List { return project.configurations - .filter { it.isCanBeResolved } - .map { configuration -> - configuration.incoming.resolutionResult.allDependencies - .filterIsInstance() - .filter { it.isRoot() } - .map { it.selected.id } - } - .filter { it.isNotEmpty() } - .flatten() + .filter { it.isCanBeResolved } + .map { configuration -> + configuration.incoming.resolutionResult.allDependencies + .filterIsInstance() + .filter { it.isRoot() } + .map { it.selected.id } + } + .filter { it.isNotEmpty() } + .flatten() } private fun resolvePomFiles( @@ -43,16 +43,16 @@ class OSSLicenseProvider { dependencyIds: List ): Map { return project.dependencies - .createArtifactResolutionQuery() - .withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java) - .forComponents(dependencyIds) - .execute() - .resolvedComponents - .flatMap { result -> - result.getArtifacts(MavenPomArtifact::class.java) - .filterIsInstance() - .map { result.id to it.file.absolutePath } - }.toMap() + .createArtifactResolutionQuery() + .withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java) + .forComponents(dependencyIds) + .execute() + .resolvedComponents + .flatMap { result -> + result.getArtifacts(MavenPomArtifact::class.java) + .filterIsInstance() + .map { result.id to it.file.absolutePath } + }.toMap() } private fun listOSSDependencies( @@ -70,67 +70,68 @@ class OSSLicenseProvider { } } - private fun readLicenseFromPomFile(path: String): OSSDependency? { - println("Reading POM from $path") + @Suppress("DefaultLocale") + private fun readLicenseFromPomFile(path: String): OSSDependency { val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(path) val identifier = readDependencyIdFromPomDocument(document) - val licenseString = readLicenseFromPomDocument(document) + val baseId = identifier.substringBeforeLast(":") + val license = readLicenseFromPomDocument(document) ?: KNOWN_LICENSES[baseId] val sourceUrl = readSourceUrlFromPomDocument(document) - val name = readDependencyNameFromPomDocument(document) - - return if (name != null) { - return OSSDependency( - name = name, - identifier = identifier, - license = licenseString ?: "Unknown", - sourceUrl = sourceUrl - ) - } else { - System.err.println("Missing groupId in $path") - null - } + val autoName = baseId.substringAfterLast(":").capitalize() + var name = readDependencyNameFromPomDocument(document) ?: autoName + if (name == baseId) name = autoName + + return OSSDependency( + name = name, + identifier = identifier, + license = license ?: "Unknown", + sourceUrl = sourceUrl + ) } private fun readDependencyIdFromPomDocument(document: Document): String { val groupIdNode = document.getElementsByTagName(TAG_GROUP_ID) - .asSequence().firstOrNull() + .asSequence().firstOrNull() val groupId = groupIdNode?.textContent val artifactIdNode = document.getElementsByTagName(TAG_ARTIFACT_ID) - .asSequence().firstOrNull() + .asSequence().firstOrNull() val artifactId = artifactIdNode?.textContent val versionNode = document.getElementsByTagName(TAG_VERSION) - .asSequence().firstOrNull() + .asSequence().firstOrNull() val version = versionNode?.textContent return "$groupId:$artifactId:$version" } private fun readSourceUrlFromPomDocument(document: Document): String? { val urlNode = document.getElementsByTagName(TAG_URL) - .asSequence().firstOrNull() - return urlNode?.textContent + .asSequence().firstOrNull() + val url = urlNode?.textContent + return if (url.isNullOrBlank() || url == "null") null else url } private fun readDependencyNameFromPomDocument(document: Document): String? { val nameNode = document.getElementsByTagName(TAG_NAME) - .asSequence().firstOrNull() - return nameNode?.textContent + .asSequence().firstOrNull() + val name = nameNode?.textContent + return if (name.isNullOrBlank() || name == "null") null else name } private fun readLicenseFromPomDocument(document: Document): String? { val licencesNode = document.getElementsByTagName(TAG_LICENSES) - .asSequence() - .firstOrNull() + .asSequence() + .firstOrNull() val licenceNodes = licencesNode?.childNodes - ?.asSequence() - ?.filter { it.nodeName == TAG_LICENSE } + ?.asSequence() + ?.filter { it.nodeName == TAG_LICENSE } val licenses = licenceNodes?.asSequence() - ?.mapNotNull { - it.childNodes - .asSequence() - .firstOrNull { child -> child.nodeName == TAG_NAME } - } - ?.joinToString("/") { it.textContent } - return licenses + ?.mapNotNull { + it.childNodes + .asSequence() + .firstOrNull { child -> child.nodeName == TAG_NAME } + } + ?.joinToString("/") { it.textContent } + + return if (licenses.isNullOrBlank() || licenses == "null") null else licenses } @Suppress("UnstableApiUsage") @@ -150,5 +151,11 @@ class OSSLicenseProvider { private const val TAG_LICENSE = "license" private const val TAG_NAME = "name" private const val TAG_URL = "url" + + private val KNOWN_LICENSES = mapOf( + "com.github.xgouchet:AXML" to "MIT License", + "com.github.medyo:android-about-page" to "MIT License", + "com.google.code.gson:gson-parent" to "The Apache Software License, Version 2.0" + ) } }