pull/2076/merge
suman9259 3 days ago committed by GitHub
commit e0d022fd24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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)
@ -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(
File(outputDirectory.get().asFile, "licenses.json"),
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,
)
}
}

@ -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

@ -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
@ -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(
@ -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,
)
}
}
}
}
@ -142,6 +154,7 @@ internal fun NiaApp(
appState: NiaAppState,
showSettingsDialog: Boolean,
onSettingsDismissed: () -> Unit,
onShowLicenses: () -> Unit,
onTopAppBarActionClick: () -> Unit,
modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
@ -152,6 +165,7 @@ internal fun NiaApp(
if (showSettingsDialog) {
SettingsDialog(
onDismiss = { onSettingsDismissed() },
onShowLicenses = onShowLicenses,
)
}

@ -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)

@ -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
@ -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,
)

@ -25,8 +25,6 @@ android {
}
dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.google.oss.licenses)
implementation(projects.core.data)
testImplementation(projects.core.testing)

@ -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" />

@ -0,0 +1,186 @@
/*
* 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 android.content.Context
import androidx.compose.foundation.layout.Arrangement
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.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.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.settings.impl.R.string
import org.json.JSONArray
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,
) {
val context = LocalContext.current
val artifacts = remember { parseLicensesJson(context) }
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 = null,
)
}
},
)
},
modifier = modifier,
) { padding ->
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxSize()
.padding(padding),
) {
items(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,
)
},
)
}
}
}
}
}
}
}
}
}
private fun parseLicensesJson(context: Context): List<LicenseArtifact> {
return try {
val json = context.assets.open("licenses.json").bufferedReader().use { it.readText() }
val array = JSONArray(json)
(0 until array.length()).map { i ->
val obj = array.getJSONObject(i)
val spdxLicenses = obj.optJSONArray("spdxLicenses")
val unknownLicenses = obj.optJSONArray("unknownLicenses")
val licenses = mutableListOf<LicenseInfo>()
if (spdxLicenses != null) {
for (j in 0 until spdxLicenses.length()) {
val license = spdxLicenses.getJSONObject(j)
licenses.add(
LicenseInfo(
name = license.optString("name", ""),
url = license.optString("url", ""),
),
)
}
}
if (unknownLicenses != null) {
for (j in 0 until unknownLicenses.length()) {
val license = unknownLicenses.getJSONObject(j)
licenses.add(
LicenseInfo(
name = license.optString("name", "Unknown"),
url = license.optString("url", ""),
),
)
}
}
LicenseArtifact(
groupId = obj.optString("groupId", ""),
artifactId = obj.optString("artifactId", ""),
version = obj.optString("version", ""),
name = if (obj.has("name")) obj.getString("name") else null,
licenses = licenses,
)
}.sortedBy { (it.name ?: "${it.groupId}:${it.artifactId}").lowercase() }
} catch (e: Exception) {
emptyList()
}
}

@ -18,7 +18,6 @@
package com.google.samples.apps.nowinandroid.feature.settings.impl
import android.content.Intent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -45,7 +44,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@ -54,7 +52,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTextButton
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.supportsDynamicTheming
@ -73,11 +70,13 @@ import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiStat
@Composable
fun SettingsDialog(
onDismiss: () -> Unit,
onShowLicenses: () -> Unit = {},
viewModel: SettingsViewModel = hiltViewModel(),
) {
val settingsUiState by viewModel.settingsUiState.collectAsStateWithLifecycle()
SettingsDialog(
onDismiss = onDismiss,
onShowLicenses = onShowLicenses,
settingsUiState = settingsUiState,
onChangeThemeBrand = viewModel::updateThemeBrand,
onChangeDynamicColorPreference = viewModel::updateDynamicColorPreference,
@ -90,6 +89,7 @@ fun SettingsDialog(
settingsUiState: SettingsUiState,
supportDynamicColor: Boolean = supportsDynamicTheming(),
onDismiss: () -> Unit,
onShowLicenses: () -> Unit = {},
onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit,
onChangeDynamicColorPreference: (useDynamicColor: Boolean) -> Unit,
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit,
@ -135,7 +135,7 @@ fun SettingsDialog(
}
}
HorizontalDivider(Modifier.padding(top = 8.dp))
LinksPanel()
LinksPanel(onShowLicenses = onShowLicenses)
}
TrackScreenViewEvent(screenName = "Settings")
},
@ -250,7 +250,7 @@ fun SettingsDialogThemeChooserRow(
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LinksPanel() {
private fun LinksPanel(onShowLicenses: () -> Unit = {}) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
@ -264,11 +264,8 @@ private fun LinksPanel() {
) {
Text(text = stringResource(string.feature_settings_impl_privacy_policy))
}
val context = LocalContext.current
NiaTextButton(
onClick = {
context.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
},
onClick = onShowLicenses,
) {
Text(text = stringResource(string.feature_settings_impl_licenses))
}

@ -40,8 +40,7 @@ firebaseBom = "33.7.0"
firebaseCrashlyticsPlugin = "3.0.6"
firebasePerfPlugin = "2.0.2"
gmsPlugin = "4.4.4"
googleOss = "17.1.0"
googleOssPlugin = "0.10.9"
licensee = "1.12.0"
hilt = "2.59"
hiltExt = "1.2.0"
jacoco = "0.8.12"
@ -128,7 +127,6 @@ firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.r
firebase-cloud-messaging = { group = "com.google.firebase", name = "firebase-messaging" }
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
firebase-performance = { group = "com.google.firebase", name = "firebase-perf" }
google-oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "googleOss" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
@ -190,7 +188,7 @@ hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
google-osslicenses = { id = "com.google.android.gms.oss-licenses-plugin", version.ref = "googleOssPlugin" }
cashapp-licensee = { id = "app.cash.licensee", version.ref = "licensee" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
room = { id = "androidx.room", version.ref = "room" }

Loading…
Cancel
Save