Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ plugins {
alias(libs.plugins.nowinandroid.android.application.jacoco)
alias(libs.plugins.nowinandroid.android.application.firebase)
alias(libs.plugins.nowinandroid.hilt)
alias(libs.plugins.google.osslicenses)
alias(libs.plugins.cashapp.licensee)
alias(libs.plugins.baselineprofile)
alias(libs.plugins.roborazzi)
alias(libs.plugins.kotlin.serialization)
Expand Down Expand Up @@ -150,3 +150,49 @@ baselineProfile {
dependencyGuard {
configuration("prodReleaseRuntimeClasspath")
}

licensee {
allow("Apache-2.0")
allow("MIT")
allow("BSD-2-Clause")
allow("BSD-3-Clause")
allow("ISC")
allow("EPL-2.0")
allowUrl("https://developer.android.com/studio/terms.html")
allowUrl("https://developers.google.com/ml-kit/terms")
}

abstract class CopyLicenseeReportTask : DefaultTask() {
@get:InputFile
abstract val inputFile: RegularFileProperty

@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty

@TaskAction
fun copy() {
inputFile.get().asFile.copyTo(
outputDirectory.get().file("licenses.json").asFile,
overwrite = true,
)
Comment on lines +174 to +177

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The copyTo function on File is a standard Java I/O operation. For better integration with Gradle's DirectoryProperty and RegularFileProperty, it's often more idiomatic to use the copy method provided by ProjectLayout or FileSystemOperations if available, or at least outputDirectory.get().file("licenses.json") to create the destination file within the DirectoryProperty context. While the current approach works, using Gradle's built-in APIs can sometimes offer better build cacheability or dependency tracking.

        inputFile.get().asFile.copyTo(
            outputDirectory.get().file("licenses.json").asFile,
            overwrite = true,
        )

}
}

androidComponents {
onVariants { variant ->
val name = variant.name.replaceFirstChar { it.uppercase() }
val task = tasks.register<CopyLicenseeReportTask>("copyLicenseeReport$name") {
dependsOn("licenseeAndroid$name")
inputFile.set(
layout.buildDirectory.file("reports/licensee/android${name}/artifacts.json"),
)
outputDirectory.set(
layout.buildDirectory.dir("generated/licenseeAssets/${variant.name}"),
)
}
variant.sources.assets?.addGeneratedSourceDirectory(
task,
CopyLicenseeReportTask::outputDirectory,
)
}
}
1 change: 0 additions & 1 deletion app/dependencies/prodReleaseRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ com.google.android.gms:play-services-measurement-impl:22.1.2
com.google.android.gms:play-services-measurement-sdk-api:22.1.2
com.google.android.gms:play-services-measurement-sdk:22.1.2
com.google.android.gms:play-services-measurement:22.1.2
com.google.android.gms:play-services-oss-licenses:17.1.0
com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.2.0
com.google.code.findbugs:jsr305:3.0.2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation.forYo
import com.google.samples.apps.nowinandroid.feature.interests.impl.navigation.interestsEntry
import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchNavKey
import com.google.samples.apps.nowinandroid.feature.search.impl.navigation.searchEntry
import com.google.samples.apps.nowinandroid.feature.settings.impl.LicensesScreen
import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsDialog
import com.google.samples.apps.nowinandroid.feature.topic.impl.navigation.topicEntry
import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS
Expand All @@ -94,6 +95,7 @@ fun NiaApp(
) {
val shouldShowGradientBackground = appState.navigationState.currentTopLevelKey == ForYouNavKey
var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
var showLicensesScreen by rememberSaveable { mutableStateOf(false) }

NiaBackground(modifier = modifier) {
NiaGradientBackground(
Expand All @@ -118,15 +120,25 @@ fun NiaApp(
}
}
CompositionLocalProvider(LocalSnackbarHostState provides snackbarHostState) {
NiaApp(
appState = appState,
if (showLicensesScreen) {
LicensesScreen(
onBackClick = { showLicensesScreen = false },
)
} else {
NiaApp(
appState = appState,

// TODO: Settings should be a dialog screen
showSettingsDialog = showSettingsDialog,
onSettingsDismissed = { showSettingsDialog = false },
onTopAppBarActionClick = { showSettingsDialog = true },
windowAdaptiveInfo = windowAdaptiveInfo,
)
// TODO: Settings should be a dialog screen
showSettingsDialog = showSettingsDialog,
onSettingsDismissed = { showSettingsDialog = false },
onShowLicenses = {
showSettingsDialog = false
showLicensesScreen = true
},
onTopAppBarActionClick = { showSettingsDialog = true },
windowAdaptiveInfo = windowAdaptiveInfo,
)
}
}
}
}
Expand All @@ -142,6 +154,7 @@ internal fun NiaApp(
appState: NiaAppState,
showSettingsDialog: Boolean,
onSettingsDismissed: () -> Unit,
onShowLicenses: () -> Unit,
onTopAppBarActionClick: () -> Unit,
modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
Expand All @@ -152,6 +165,7 @@ internal fun NiaApp(
if (showSettingsDialog) {
SettingsDialog(
onDismiss = { onSettingsDismissed() },
onShowLicenses = onShowLicenses,
)
}

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ plugins {
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.google.osslicenses) apply false
alias(libs.plugins.cashapp.licensee) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.spotless) apply false
alias(libs.plugins.nowinandroid.root)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
Expand Down Expand Up @@ -85,14 +84,14 @@ fun NiaTabRow(
modifier: Modifier = Modifier,
tabs: @Composable () -> Unit,
) {
TabRow(
SecondaryTabRow(
selectedTabIndex = selectedTabIndex,
modifier = modifier,
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface,
indicator = { tabPositions ->
indicator = {
TabRowDefaults.SecondaryIndicator(
modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
modifier = Modifier.tabIndicatorOffset(selectedTabIndex),
height = 2.dp,
color = MaterialTheme.colorScheme.onSurface,
)
Expand Down
2 changes: 0 additions & 2 deletions feature/settings/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ android {
}

dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.google.oss.licenses)
implementation(projects.core.data)

testImplementation(projects.core.testing)
Expand Down
12 changes: 1 addition & 11 deletions feature/settings/impl/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,4 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
android:theme="@style/Theme.AppCompat" />

<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
android:theme="@style/Theme.AppCompat" />
</application>
</manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.samples.apps.nowinandroid.feature.settings.impl

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.settings.impl.R.string

data class LicenseArtifact(
val groupId: String,
val artifactId: String,
val version: String,
val name: String?,
val licenses: List<LicenseInfo>,
)

data class LicenseInfo(
val name: String,
val url: String,
)

@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun LicensesScreen(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
viewModel: LicensesViewModel = hiltViewModel(),
) {
val uiState by viewModel.licensesUiState.collectAsStateWithLifecycle()

LicensesScreen(
uiState = uiState,
onBackClick = onBackClick,
modifier = modifier,
)
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
internal fun LicensesScreen(
uiState: LicensesUiState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val uriHandler = LocalUriHandler.current

Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(string.feature_settings_impl_licenses)) },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = NiaIcons.ArrowBack,
contentDescription = stringResource(string.feature_settings_impl_back),
)
}
},
)
},
modifier = modifier,
) { padding ->
when (uiState) {
LicensesUiState.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
is LicensesUiState.Success -> {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxSize()
.padding(padding),
) {
items(uiState.artifacts, key = { "${it.groupId}:${it.artifactId}" }) { artifact ->
Card(
modifier = Modifier.fillMaxWidth(),
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = artifact.name
?: "${artifact.groupId}:${artifact.artifactId}",
style = MaterialTheme.typography.titleSmall,
)
Text(
text = "${artifact.groupId}:${artifact.artifactId}:${artifact.version}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (artifact.licenses.isNotEmpty()) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
artifact.licenses.forEach { license ->
SuggestionChip(
onClick = {
if (license.url.isNotBlank()) {
uriHandler.openUri(license.url)
}
},
label = {
Text(
text = license.name,
style = MaterialTheme.typography.labelSmall,
)
},
)
}
}
}
}
}
}
}
}
}
}
}
Loading