Merge branch 'android:main' into main

pull/812/head
İbrahim Ethem Şen 1 year ago committed by GitHub
commit 6271d1c4c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,49 +0,0 @@
name: Android CI with GMD
on:
push:
branches:
- main
pull_request:
jobs:
android-ci:
runs-on: macos-12
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Build AndroidTest apps
run: ./gradlew packageDemoDebug packageDemoDebugAndroidTest
- name: Run instrumented tests with GMD
run: ./gradlew cleanManagedDevices --unused-only &&
./gradlew ciDemoDebugAndroidTest -Dorg.gradle.workers.max=1
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
- name: Upload test reports
if: success() || failure()
uses: actions/upload-artifact@v3
with:
name: test-reports
path: '**/build/reports/androidTests'

@ -5,6 +5,7 @@ on:
branches:
- main
pull_request:
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
@ -108,3 +109,42 @@ jobs:
with:
name: test-reports-${{ matrix.api-level }}
path: '**/build/reports/androidTests'
androidTest-GMD:
needs: build
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
timeout-minutes: 55
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Accept Android licenses
run: yes | "$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager --licenses || true
- name: Build AndroidTest apps
run: ./gradlew packageDemoDebug packageDemoDebugAndroidTest
- name: Run instrumented tests with GMD
run: ./gradlew cleanManagedDevices --unused-only &&
./gradlew ciDemoDebugAndroidTest -Dorg.gradle.workers.max=1
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
- name: Upload test reports
if: success() || failure()
uses: actions/upload-artifact@v3
with:
name: test-reports
path: '**/build/reports/androidTests'

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -45,11 +45,6 @@ understanding of which libraries and tools are being used, the reasoning behind
UI, testing, architecture and more, and how all of these different pieces of the project fit
together to create a complete app.
NOTE: Building the app using an M1 Mac will require the use of
[Rosetta](https://support.apple.com/en-gb/HT211861). See
[the following bug](https://github.com/protocolbuffers/protobuf/issues/9397#issuecomment-1086138036)
for more details.
# Architecture
The **Now in Android** app follows the

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -1,30 +1,3 @@
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider

@ -33,6 +33,7 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException
import com.google.samples.apps.nowinandroid.MainActivity
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
@ -66,9 +67,15 @@ class NavigationTest {
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/**
* Use the primary activity to initialize the app normally.
* Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
*/
@get:Rule(order = 2)
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
/**
* Use the primary activity to initialize the app normally.
*/
@get:Rule(order = 3)
val composeTestRule = createAndroidComposeRule<MainActivity>()
private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) =

@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.testharness.TestHarness
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
@ -61,9 +62,15 @@ class NavigationUiTest {
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/**
* Use a test activity to set the content on.
* Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
*/
@get:Rule(order = 2)
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
/**
* Use a test activity to set the content on.
*/
@get:Rule(order = 3)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
val userNewsResourceRepository = CompositeUserNewsResourceRepository(

@ -0,0 +1,62 @@
/*
* Copyright 2023 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.interests
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import com.google.samples.apps.nowinandroid.allowNotifications
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ScrollTopicListBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun benchmarkStateChangeCompilationBaselineProfile() =
benchmarkStateChange(CompilationMode.Partial())
private fun benchmarkStateChange(compilationMode: CompilationMode) =
benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics = listOf(FrameTimingMetric()),
compilationMode = compilationMode,
iterations = 10,
startupMode = StartupMode.WARM,
setupBlock = {
// Start the app
pressHome()
startActivityAndWait()
allowNotifications()
// Navigate to interests screen
device.findObject(By.text("Interests")).click()
device.waitForIdle()
},
) {
interestsWaitForTopics()
repeat(3) {
interestsScrollTopicsDownUp()
}
}
}

@ -16,12 +16,11 @@
import com.android.build.api.dsl.ApplicationExtension
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
@ -32,7 +31,6 @@ class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
apply("com.google.firebase.crashlytics")
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
val bom = libs.findLibrary("firebase-bom").get()
add("implementation", platform(bom))

@ -14,15 +14,13 @@
* limitations under the License.
*/
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.gradle.LibraryExtension
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.kotlin
class AndroidFeatureConventionPlugin : Plugin<Project> {
@ -40,8 +38,6 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
configureGradleManagedDevices(this)
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
add("implementation", project(":core:model"))
add("implementation", project(":core:ui"))

@ -14,11 +14,10 @@
* limitations under the License.
*/
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
class AndroidHiltConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
@ -30,7 +29,6 @@ class AndroidHiltConventionPlugin : Plugin<Project> {
apply("org.jetbrains.kotlin.kapt")
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
"implementation"(libs.findLibrary("hilt.android").get())
"kapt"(libs.findLibrary("hilt.compiler").get())

@ -21,12 +21,11 @@ import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask
import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.kotlin
class AndroidLibraryConventionPlugin : Plugin<Project> {
@ -47,7 +46,6 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
configurePrintApksTask(this)
disableUnnecessaryAndroidTests(target)
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
configurations.configureEach {
resolutionStrategy {
force(libs.findLibrary("junit4").get())

@ -15,15 +15,14 @@
*/
import com.google.devtools.ksp.gradle.KspExtension
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.process.CommandLineArgumentProvider
import java.io.File
@ -40,7 +39,6 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
add("implementation", libs.findLibrary("room.runtime").get())
add("implementation", libs.findLibrary("room.ktx").get())

@ -18,9 +18,7 @@ package com.google.samples.apps.nowinandroid
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File
@ -31,8 +29,6 @@ import java.io.File
internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension<*, *, *, *>,
) {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
commonExtension.apply {
buildFeatures {
compose = true

@ -18,10 +18,8 @@ package com.google.samples.apps.nowinandroid
import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.withType
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
@ -44,8 +42,6 @@ private fun String.capitalize() = replaceFirstChar {
internal fun Project.configureJacoco(
androidComponentsExtension: AndroidComponentsExtension<*, *, *>,
) {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
configure<JacocoPluginExtension> {
toolVersion = libs.findVersion("jacoco").get().toString()
}

@ -19,11 +19,9 @@ package com.google.samples.apps.nowinandroid
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.provideDelegate
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@ -52,8 +50,6 @@ internal fun Project.configureKotlinAndroid(
configureKotlin()
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get())
}
@ -88,6 +84,9 @@ private fun Project.configureKotlin() {
allWarningsAsErrors = warningsAsErrors.toBoolean()
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.RequiresOptIn",
// Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
)
}
}

@ -0,0 +1,25 @@
/*
* Copyright 2023 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
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
val Project.libs
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")

@ -21,6 +21,7 @@ import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.NetworkRequest.Builder
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
@ -44,36 +45,33 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
}
/**
* Sends the latest connectivity status to the underlying channel.
*/
fun update() {
channel.trySend(connectivityManager.isCurrentlyConnected())
}
/**
* The callback's methods are invoked on changes to *any* network, not just the active
* network. So to check for network connectivity, one must query the active network of the
* ConnectivityManager.
* The callback's methods are invoked on changes to *any* network matching the [NetworkRequest],
* not just the active network. So we can simply track the presence (or absence) of such [Network].
*/
val callback = object : NetworkCallback() {
override fun onAvailable(network: Network) = update()
override fun onLost(network: Network) = update()
private val networks = mutableSetOf<Network>()
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) = update()
override fun onAvailable(network: Network) {
networks += network
channel.trySend(true)
}
override fun onLost(network: Network) {
networks -= network
channel.trySend(networks.isNotEmpty())
}
}
connectivityManager.registerNetworkCallback(
Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build(),
callback,
)
val request = Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
update()
/**
* Sends the latest connectivity status to the underlying channel.
*/
channel.trySend(connectivityManager.isCurrentlyConnected())
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
@ -87,6 +85,7 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
activeNetwork
?.let(::getNetworkCapabilities)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
else -> activeNetworkInfo?.isConnected
} ?: false
}

@ -0,0 +1,29 @@
/*
* Copyright 2023 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.core.rules
import android.Manifest.permission.POST_NOTIFICATIONS
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.TIRAMISU
import androidx.test.rule.GrantPermissionRule.grant
import org.junit.rules.TestRule
/**
* [TestRule] granting [POST_NOTIFICATIONS] permission if running on [SDK_INT] greater than [TIRAMISU].
*/
class GrantPostNotificationsPermissionRule :
TestRule by if (SDK_INT >= TIRAMISU) grant(POST_NOTIFICATIONS) else grant()

@ -57,7 +57,11 @@ fun LazyGridScope.newsFeed(
when (feedState) {
NewsFeedUiState.Loading -> Unit
is NewsFeedUiState.Success -> {
items(feedState.feed, key = { it.id }) { userNewsResource ->
items(
items = feedState.feed,
key = { it.id },
contentType = { "newsFeedItem" },
) { userNewsResource ->
val resourceUrl by remember {
mutableStateOf(Uri.parse(userNewsResource.url))
}

@ -159,13 +159,21 @@ Using the above modularization strategy, the Now in Android app has the followin
<td><code>TopicsRepository</code><br>
</td>
</tr>
<tr>
<td><code>core:designsystem</code>
</td>
<td>Design system which includes Core UI components (many of which are customized Material 3 components), app theme and icons. The design system can be viewed by running the <code>app-nia-catalog</code> run configuration.
</td>
<td>
<code>NiaIcons</code> <code>NiaButton</code> <code>NiaTheme</code>
</td>
</tr>
<tr>
<td><code>core:ui</code>
</td>
<td>UI components, composables and resources, such as icons, used by different features.
<td>Composite UI components and resources used by feature modules, such as the news feed. Unlike the <code>designsystem</code> module, it is dependent on the data layer since it renders models, like news resources.
</td>
<td><code>NiaIcons</code><br>
<code>NewsResourceCardExpanded</code>
<td> <code>NewsFeed</code> <code>NewsResourceCardExpanded</code>
</td>
</tr>
<tr>

@ -16,9 +16,6 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import android.Manifest
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.TIRAMISU
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.ui.test.assertHasClickAction
@ -31,8 +28,7 @@ import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollToNode
import androidx.test.rule.GrantPermissionRule
import androidx.test.rule.GrantPermissionRule.grant
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData
import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
@ -41,15 +37,10 @@ import org.junit.Test
class ForYouScreenTest {
@get:Rule
val permissionTestRule: GrantPermissionRule =
if (SDK_INT >= TIRAMISU) {
grant(Manifest.permission.POST_NOTIFICATIONS)
} else {
grant()
}
@get:Rule(order = 0)
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
@get:Rule
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
private val doneButtonMatcher by lazy {

@ -68,6 +68,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -181,7 +182,7 @@ internal fun ForYouScreen(
onTopicClick = onTopicClick,
)
item(span = { GridItemSpan(maxLineSpan) }) {
item(span = { GridItemSpan(maxLineSpan) }, contentType = "bottomSpacing") {
Column {
Spacer(modifier = Modifier.height(8.dp))
// Add space for the content to clear the "offline" snackbar.
@ -239,7 +240,7 @@ private fun LazyGridScope.onboarding(
-> Unit
is OnboardingUiState.Shown -> {
item(span = { GridItemSpan(maxLineSpan) }) {
item(span = { GridItemSpan(maxLineSpan) }, contentType = "onboarding") {
Column(modifier = interestsItemModifier) {
Text(
text = stringResource(R.string.onboarding_guidance_title),
@ -406,6 +407,9 @@ fun TopicIcon(
@Composable
@OptIn(ExperimentalPermissionsApi::class)
private fun NotificationPermissionEffect() {
// Permission requests should only be made from an Activity Context, which is not present
// in previews
if (LocalInspectionMode.current) return
if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) return
val notificationsPermissionState = rememberPermissionState(
android.Manifest.permission.POST_NOTIFICATIONS,

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -17,9 +17,11 @@
package com.google.samples.apps.nowinandroid.feature.settings
import android.content.Intent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
@ -149,8 +151,9 @@ fun SettingsDialog(
)
}
// [ColumnScope] is used for using the [ColumnScope.AnimatedVisibility] extension overload composable.
@Composable
private fun SettingsPanel(
private fun ColumnScope.SettingsPanel(
settings: UserEditableSettings,
supportDynamicColor: Boolean,
onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit,
@ -170,19 +173,21 @@ private fun SettingsPanel(
onClick = { onChangeThemeBrand(ANDROID) },
)
}
if (settings.brand == DEFAULT && supportDynamicColor) {
SettingsDialogSectionTitle(text = stringResource(R.string.dynamic_color_preference))
Column(Modifier.selectableGroup()) {
SettingsDialogThemeChooserRow(
text = stringResource(string.dynamic_color_yes),
selected = settings.useDynamicColor,
onClick = { onChangeDynamicColorPreference(true) },
)
SettingsDialogThemeChooserRow(
text = stringResource(string.dynamic_color_no),
selected = !settings.useDynamicColor,
onClick = { onChangeDynamicColorPreference(false) },
)
AnimatedVisibility(visible = settings.brand == DEFAULT && supportDynamicColor) {
Column {
SettingsDialogSectionTitle(text = stringResource(R.string.dynamic_color_preference))
Column(Modifier.selectableGroup()) {
SettingsDialogThemeChooserRow(
text = stringResource(string.dynamic_color_yes),
selected = settings.useDynamicColor,
onClick = { onChangeDynamicColorPreference(true) },
)
SettingsDialogThemeChooserRow(
text = stringResource(string.dynamic_color_no),
selected = !settings.useDynamicColor,
onClick = { onChangeDynamicColorPreference(false) },
)
}
}
}
SettingsDialogSectionTitle(text = stringResource(R.string.dark_mode_preference))

@ -22,7 +22,7 @@ org.gradle.configureondemand=false
org.gradle.caching=true
# Enable configuration caching between builds.
org.gradle.unsafe.configuration-cache=true
org.gradle.configuration-cache=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
@ -35,9 +35,6 @@ kotlin.code.style=official
android.nonTransitiveRClass=true
# Disable build features that are enabled by default,
# https://developer.android.com/studio/releases/gradle-plugin#buildFeatures
android.defaults.buildfeatures.buildconfig=false
android.defaults.buildfeatures.aidl=false
android.defaults.buildfeatures.renderscript=false
# https://developer.android.com/build/releases/gradle-plugin#default-changes
android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false

@ -42,7 +42,7 @@ junit4 = "4.13.2"
kotlin = "1.8.20"
kotlinxCoroutines = "1.6.4"
kotlinxDatetime = "0.4.0"
kotlinxSerializationJson = "1.5.0"
kotlinxSerializationJson = "1.5.1"
ksp = "1.8.20-1.0.11"
lint = "30.3.1"
okhttp = "4.10.0"

Loading…
Cancel
Save