Skip to content
This repository has been archived by the owner on Nov 12, 2024. It is now read-only.

Commit

Permalink
Get open source licenses screen working on iOS (#1537)
Browse files Browse the repository at this point in the history
* Tidy ups in :data:licenses

* Add iOS support for displaying licenses

* Improve Licenses UI

Add some grouping and sticky headers

* Fix Spotless on build-logic

* Move Licensee copy logic to Gradle

* Fix rebase
  • Loading branch information
chrisbanes authored Sep 14, 2023
1 parent 4a12a1c commit a7128a5
Show file tree
Hide file tree
Showing 29 changed files with 298 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

@Composable
Expand Down Expand Up @@ -87,12 +88,17 @@ fun Preference(
}

@Composable
fun PreferenceHeader(title: String) {
Surface {
fun PreferenceHeader(
title: String,
modifier: Modifier = Modifier,
tonalElevation: Dp = 0.dp,
) {
Surface(modifier = modifier, tonalElevation = tonalElevation) {
Text(
text = title,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 4.dp),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ val EnTiviStrings = TiviStrings(
settingsIgnoreSpecialsTitle = "Ignore specials",
settingsIgnoreSpecialsSummary = "Automatically ignore specials",
settingsOpenSource = "Open source licenses",
settingsOpenSourceSummary = "Tivi 💞open source",
settingsOpenSourceSummary = "Tivi 💞 open source",
settingsThemeTitle = "Theme",
settingsTitle = "Settings",
settingsUiCategoryTitle = "User Interface",
Expand All @@ -175,7 +175,5 @@ val EnTiviStrings = TiviStrings(
upnextFilterFollowedShowsOnlyTitle = "Followed only",
upnextTitle = "Up Next",
viewPrivacyPolicy = "View Privacy Policy",
viewOpenSourceLicenses = "View Open Source Licenses",
openSourceLicensesTitle = "Open Source Licenses",
watchedShowsTitle = "Watched",
)
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,6 @@ data class TiviStrings(
val upnextFilterFollowedShowsOnlyTitle: String,
val upnextTitle: String,
val viewPrivacyPolicy: String,
val viewOpenSourceLicenses: String,
val openSourceLicensesTitle: String,
val watchedShowsTitle: String,
)

Expand Down
9 changes: 2 additions & 7 deletions data/licenses/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,18 @@
plugins {
id("app.tivi.android.library")
id("app.tivi.kotlin.multiplatform")
kotlin("plugin.serialization") version "1.9.10"
alias(libs.plugins.kotlin.serialization)
}

kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(projects.core.base)
api(projects.core.logging.api)
implementation(libs.kotlinx.serialization)
}
}

val androidMain by getting {
dependencies {
implementation(libs.androidx.core)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ actual interface LicenseDataPlatformComponent {

@ApplicationScope
@Provides
fun provideLicensesFetcher(fetcher: AndroidLicensesFetcherImpl): LicensesFetcher = fetcher
fun bindLicensesFetcher(fetcher: AndroidLicensesFetcherImpl): LicensesFetcher = fetcher
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,24 @@ package app.tivi.data.licenses.fetcher

import android.app.Application
import app.tivi.data.licenses.LicenseItem
import java.io.IOException
import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import app.tivi.util.AppCoroutineDispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import me.tatarka.inject.annotations.Inject

@Inject
class AndroidLicensesFetcherImpl(private val context: Application) : LicensesFetcher {
@kotlinx.serialization.ExperimentalSerializationApi
override suspend fun fetch(): List<LicenseItem> {
var licenseItemList: List<LicenseItem> = emptyList()
withContext(Dispatchers.IO) {
try {
val inputStream: InputStream = context.assets.open("artifacts.json")
val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
licenseItemList = json.decodeFromStream(inputStream)
} catch (ex: IOException) {
licenseItemList = emptyList()
}
class AndroidLicensesFetcherImpl(
private val context: Application,
private val dispatchers: AppCoroutineDispatchers,
) : LicensesFetcher {
@ExperimentalSerializationApi
override suspend fun invoke(): List<LicenseItem> = withContext(dispatchers.io) {
val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
return licenseItemList
json.decodeFromStream(context.assets.open("licenses.json"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ expect interface LicenseDataPlatformComponent
interface LicenseDataComponent : LicenseDataPlatformComponent {
@ApplicationScope
@Provides
fun providePreferences(bind: LicensesStoreImpl): LicensesStore = bind
fun bindLicensesStore(bind: LicensesStoreImpl): LicensesStore = bind
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,6 @@ package app.tivi.data.licenses

import kotlinx.serialization.Serializable

data class LicensesState(
val licenseItemList: List<LicenseItem>,
)

/**
* {
* "groupId": "com.google.firebase",
* "artifactId": "firebase-crashlytics",
* "version": "18.4.1",
* "spdxLicenses": [
* {
* "identifier": "Apache-2.0",
* "name": "Apache License 2.0",
* "url": "https://www.apache.org/licenses/LICENSE-2.0"
* }
* ],
* "scm": {
* "url": "https://github.com/firebase/firebase-android-sdk"
* }
* },
*/

@Serializable
data class LicenseItem(
val groupId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ package app.tivi.data.licenses.fetcher
import app.tivi.data.licenses.LicenseItem

interface LicensesFetcher {
suspend fun fetch(): List<LicenseItem>
suspend operator fun invoke(): List<LicenseItem>
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
package app.tivi.data.licenses.store

import app.tivi.data.licenses.LicenseItem
import me.tatarka.inject.annotations.Inject

@Inject
interface LicensesStore {
suspend fun getOpenSourceItemList(): List<LicenseItem>
suspend fun getLicenses(): List<LicenseItem>
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ package app.tivi.data.licenses.store

import app.tivi.data.licenses.LicenseItem
import app.tivi.data.licenses.fetcher.LicensesFetcher
import app.tivi.util.Logger
import me.tatarka.inject.annotations.Inject

@Inject
class LicensesStoreImpl(private val fetcher: LicensesFetcher) : LicensesStore {
class LicensesStoreImpl(
private val fetcher: LicensesFetcher,
private val logger: Logger,
) : LicensesStore {
private var licenses: List<LicenseItem>? = null

override suspend fun getOpenSourceItemList(): List<LicenseItem> {
return fetcher.fetch()
override suspend fun getLicenses(): List<LicenseItem> {
return licenses ?: try {
fetcher().also { licenses = it }
} catch (e: Exception) {
logger.e(e) { "Exception whilst fetching licenses" }
emptyList()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ actual interface LicenseDataPlatformComponent {

@ApplicationScope
@Provides
fun provideLicensesFetcher(fetcher: IosLicensesFetcherImpl): LicensesFetcher = fetcher
fun bindLicensesFetcher(fetcher: IosLicensesFetcherImpl): LicensesFetcher = fetcher
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,45 @@
package app.tivi.data.licenses.fetcher

import app.tivi.data.licenses.LicenseItem
import app.tivi.util.AppCoroutineDispatchers
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject
import platform.Foundation.NSBundle
import platform.Foundation.NSData
import platform.Foundation.NSFileManager
import platform.posix.memcpy

@OptIn(ExperimentalSerializationApi::class)
@Inject
class IosLicensesFetcherImpl : LicensesFetcher {
override suspend fun fetch(): List<LicenseItem> {
return emptyList()
class IosLicensesFetcherImpl(
private val dispatchers: AppCoroutineDispatchers,
) : LicensesFetcher {
override suspend fun invoke(): List<LicenseItem> = withContext(dispatchers.io) {
val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
json.decodeFromString(readBundleFile("licenses.json").decodeToString())
}
}

@OptIn(ExperimentalForeignApi::class)
private fun readBundleFile(path: String): ByteArray {
val fileManager = NSFileManager.defaultManager()
val composeResourcesPath = NSBundle.mainBundle.resourcePath + "/" + path
val contentsAtPath: NSData? = fileManager.contentsAtPath(composeResourcesPath)
if (contentsAtPath != null) {
val byteArray = ByteArray(contentsAtPath.length.toInt())
byteArray.usePinned {
memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length)
}
return byteArray
} else {
error("File $path not found in Bundle")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ actual interface LicenseDataPlatformComponent {

@ApplicationScope
@Provides
fun provideLicensesFetcher(fetcher: JvmLicensesFetcherImpl): LicensesFetcher = fetcher
fun bindLicensesFetcher(fetcher: JvmLicensesFetcherImpl): LicensesFetcher = fetcher
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import me.tatarka.inject.annotations.Inject

@Inject
class JvmLicensesFetcherImpl : LicensesFetcher {
override suspend fun fetch(): List<LicenseItem> {
override suspend fun invoke(): List<LicenseItem> {
return emptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ class FetchLicensesList(
) : Interactor<Unit, List<LicenseItem>>() {

override suspend fun doWork(params: Unit): List<LicenseItem> {
return licensesStore.getOpenSourceItemList()
return licensesStore.getLicenses()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,21 @@

package app.tivi.gradle

import app.cash.licensee.LicenseeExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.reporting.ReportingExtension
import org.gradle.kotlin.dsl.configure

class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.application")
apply("org.gradle.android.cache-fix")
apply("app.cash.licensee")
configure<LicenseeExtension> {
allow("Apache-2.0")
allow("MIT")
allow("BSD-3-Clause")
allowUrl("https://developer.android.com/studio/terms.html")
}
}
val reportingExtension: ReportingExtension =
project.extensions.getByType(ReportingExtension::class.java)

configureAndroid()
configureLauncherTasks()
configureLicensesTasks(reportingExtension)
configureLicensee()
configureAndroidLicensesTasks()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,37 @@ package app.tivi.gradle

import app.tivi.gradle.task.AssetCopyTask
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.gradle.internal.tasks.factory.dependsOn
import java.util.Locale
import org.gradle.api.Project
import org.gradle.api.reporting.ReportingExtension
import org.gradle.configurationcache.extensions.capitalized
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.register

fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
fun Project.configureAndroidLicensesTasks() {
val reportingExtension = project.extensions.getByType(ReportingExtension::class.java)

androidComponents {
onVariants { variant ->
val capitalizedVariantName = variant.name.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
val capitalizedVariantName = variant.name.capitalized()

val artifactsFile = reportingExtension.file("licensee/${variant.name}/artifacts.json")

val copyArtifactsTask =
project.tasks.register<AssetCopyTask>("copy${capitalizedVariantName}LicenseeReportToAssets") {
inputFile.set(artifactsFile)
targetFileName.set("artifacts.json")
}
val copyArtifactsTask = tasks.register<AssetCopyTask>(
"copy${capitalizedVariantName}LicenseeOutputToAndroidAssets",
) {
inputFile.set(artifactsFile)
outputFilename.set("licenses.json")

dependsOn("licensee$capitalizedVariantName")
}

variant.sources.assets?.addGeneratedSourceDirectory(
copyArtifactsTask,
AssetCopyTask::outputDirectory,
)
copyArtifactsTask.dependsOn("licensee$capitalizedVariantName")
}
}
}

private fun Project.androidComponents(action: ApplicationAndroidComponentsExtension.() -> Unit) =
extensions.configure<ApplicationAndroidComponentsExtension>(action)
extensions.configure(action)
Loading

0 comments on commit a7128a5

Please sign in to comment.