Merge branch 'android:main' into main

pull/812/head
İbrahim Ethem Şen 2 years 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: branches:
- main - main
pull_request: pull_request:
concurrency: concurrency:
group: build-${{ github.ref }} group: build-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -108,3 +109,42 @@ jobs:
with: with:
name: test-reports-${{ matrix.api-level }} name: test-reports-${{ matrix.api-level }}
path: '**/build/reports/androidTests' 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 UI, testing, architecture and more, and how all of these different pieces of the project fit
together to create a complete app. 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 # Architecture
The **Now in Android** app follows the 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.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket -dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider

@ -33,6 +33,7 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException import androidx.test.espresso.NoActivityResumedException
import com.google.samples.apps.nowinandroid.MainActivity import com.google.samples.apps.nowinandroid.MainActivity
import com.google.samples.apps.nowinandroid.R 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.BindValue
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
@ -66,9 +67,15 @@ class NavigationTest {
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() 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) @get:Rule(order = 2)
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
/**
* Use the primary activity to initialize the app normally.
*/
@get:Rule(order = 3)
val composeTestRule = createAndroidComposeRule<MainActivity>() val composeTestRule = createAndroidComposeRule<MainActivity>()
private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = 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.accompanist.testharness.TestHarness
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository 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.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.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
@ -61,9 +62,15 @@ class NavigationUiTest {
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() 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) @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 composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
val userNewsResourceRepository = CompositeUserNewsResourceRepository( 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.android.build.api.dsl.ApplicationExtension
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> { class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
@ -32,7 +31,6 @@ class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
apply("com.google.firebase.crashlytics") apply("com.google.firebase.crashlytics")
} }
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies { dependencies {
val bom = libs.findLibrary("firebase-bom").get() val bom = libs.findLibrary("firebase-bom").get()
add("implementation", platform(bom)) add("implementation", platform(bom))

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

@ -14,11 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
class AndroidHiltConventionPlugin : Plugin<Project> { class AndroidHiltConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
@ -30,7 +29,6 @@ class AndroidHiltConventionPlugin : Plugin<Project> {
apply("org.jetbrains.kotlin.kapt") apply("org.jetbrains.kotlin.kapt")
} }
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies { dependencies {
"implementation"(libs.findLibrary("hilt.android").get()) "implementation"(libs.findLibrary("hilt.android").get())
"kapt"(libs.findLibrary("hilt.compiler").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.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.configurePrintApksTask
import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.kotlin import org.gradle.kotlin.dsl.kotlin
class AndroidLibraryConventionPlugin : Plugin<Project> { class AndroidLibraryConventionPlugin : Plugin<Project> {
@ -47,7 +46,6 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
configurePrintApksTask(this) configurePrintApksTask(this)
disableUnnecessaryAndroidTests(target) disableUnnecessaryAndroidTests(target)
} }
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
configurations.configureEach { configurations.configureEach {
resolutionStrategy { resolutionStrategy {
force(libs.findLibrary("junit4").get()) force(libs.findLibrary("junit4").get())

@ -15,15 +15,14 @@
*/ */
import com.google.devtools.ksp.gradle.KspExtension import com.google.devtools.ksp.gradle.KspExtension
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.PathSensitivity
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.process.CommandLineArgumentProvider import org.gradle.process.CommandLineArgumentProvider
import java.io.File import java.io.File
@ -40,7 +39,6 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
arg(RoomSchemaArgProvider(File(projectDir, "schemas"))) arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
} }
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies { dependencies {
add("implementation", libs.findLibrary("room.runtime").get()) add("implementation", libs.findLibrary("room.runtime").get())
add("implementation", libs.findLibrary("room.ktx").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 com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File import java.io.File
@ -31,8 +29,6 @@ import java.io.File
internal fun Project.configureAndroidCompose( internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension<*, *, *, *>, commonExtension: CommonExtension<*, *, *, *>,
) { ) {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
commonExtension.apply { commonExtension.apply {
buildFeatures { buildFeatures {
compose = true compose = true

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

@ -19,11 +19,9 @@ package com.google.samples.apps.nowinandroid
import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion import org.gradle.api.JavaVersion
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.provideDelegate
import org.gradle.kotlin.dsl.withType import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@ -52,8 +50,6 @@ internal fun Project.configureKotlinAndroid(
configureKotlin() configureKotlin()
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies { dependencies {
add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get()) add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get())
} }
@ -88,6 +84,9 @@ private fun Project.configureKotlin() {
allWarningsAsErrors = warningsAsErrors.toBoolean() allWarningsAsErrors = warningsAsErrors.toBoolean()
freeCompilerArgs = freeCompilerArgs + listOf( freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.RequiresOptIn", "-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.ConnectivityManager.NetworkCallback
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.NetworkRequest.Builder import android.net.NetworkRequest.Builder
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES
@ -44,36 +45,33 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
} }
/** /**
* Sends the latest connectivity status to the underlying channel. * 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].
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.
*/ */
val callback = object : NetworkCallback() { val callback = object : NetworkCallback() {
override fun onAvailable(network: Network) = update()
override fun onLost(network: Network) = update() private val networks = mutableSetOf<Network>()
override fun onAvailable(network: Network) {
networks += network
channel.trySend(true)
}
override fun onCapabilitiesChanged( override fun onLost(network: Network) {
network: Network, networks -= network
networkCapabilities: NetworkCapabilities, channel.trySend(networks.isNotEmpty())
) = update() }
} }
connectivityManager.registerNetworkCallback( val request = Builder()
Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build(), .build()
callback, connectivityManager.registerNetworkCallback(request, callback)
)
update() /**
* Sends the latest connectivity status to the underlying channel.
*/
channel.trySend(connectivityManager.isCurrentlyConnected())
awaitClose { awaitClose {
connectivityManager.unregisterNetworkCallback(callback) connectivityManager.unregisterNetworkCallback(callback)
@ -87,6 +85,7 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
activeNetwork activeNetwork
?.let(::getNetworkCapabilities) ?.let(::getNetworkCapabilities)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
else -> activeNetworkInfo?.isConnected else -> activeNetworkInfo?.isConnected
} ?: false } ?: 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) { when (feedState) {
NewsFeedUiState.Loading -> Unit NewsFeedUiState.Loading -> Unit
is NewsFeedUiState.Success -> { is NewsFeedUiState.Success -> {
items(feedState.feed, key = { it.id }) { userNewsResource -> items(
items = feedState.feed,
key = { it.id },
contentType = { "newsFeedItem" },
) { userNewsResource ->
val resourceUrl by remember { val resourceUrl by remember {
mutableStateOf(Uri.parse(userNewsResource.url)) 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><code>TopicsRepository</code><br>
</td> </td>
</tr> </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> <tr>
<td><code>core:ui</code> <td><code>core:ui</code>
</td> </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>
<td><code>NiaIcons</code><br> <td> <code>NewsFeed</code> <code>NewsResourceCardExpanded</code>
<code>NewsResourceCardExpanded</code>
</td> </td>
</tr> </tr>
<tr> <tr>

@ -16,9 +16,6 @@
package com.google.samples.apps.nowinandroid.feature.foryou 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.activity.ComponentActivity
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.ui.test.assertHasClickAction 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.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performScrollToNode
import androidx.test.rule.GrantPermissionRule import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
import androidx.test.rule.GrantPermissionRule.grant
import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData 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.testing.data.userNewsResourcesTestData
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
@ -41,15 +37,10 @@ import org.junit.Test
class ForYouScreenTest { class ForYouScreenTest {
@get:Rule @get:Rule(order = 0)
val permissionTestRule: GrantPermissionRule = val postNotificationsPermission = GrantPostNotificationsPermissionRule()
if (SDK_INT >= TIRAMISU) {
grant(Manifest.permission.POST_NOTIFICATIONS)
} else {
grant()
}
@get:Rule @get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<ComponentActivity>() val composeTestRule = createAndroidComposeRule<ComponentActivity>()
private val doneButtonMatcher by lazy { 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.layout.layout
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -181,7 +182,7 @@ internal fun ForYouScreen(
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
) )
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }, contentType = "bottomSpacing") {
Column { Column {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// Add space for the content to clear the "offline" snackbar. // Add space for the content to clear the "offline" snackbar.
@ -239,7 +240,7 @@ private fun LazyGridScope.onboarding(
-> Unit -> Unit
is OnboardingUiState.Shown -> { is OnboardingUiState.Shown -> {
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }, contentType = "onboarding") {
Column(modifier = interestsItemModifier) { Column(modifier = interestsItemModifier) {
Text( Text(
text = stringResource(R.string.onboarding_guidance_title), text = stringResource(R.string.onboarding_guidance_title),
@ -406,6 +407,9 @@ fun TopicIcon(
@Composable @Composable
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
private fun NotificationPermissionEffect() { 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 if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) return
val notificationsPermissionState = rememberPermissionState( val notificationsPermissionState = rememberPermissionState(
android.Manifest.permission.POST_NOTIFICATIONS, 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 package com.google.samples.apps.nowinandroid.feature.settings
import android.content.Intent import android.content.Intent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -149,8 +151,9 @@ fun SettingsDialog(
) )
} }
// [ColumnScope] is used for using the [ColumnScope.AnimatedVisibility] extension overload composable.
@Composable @Composable
private fun SettingsPanel( private fun ColumnScope.SettingsPanel(
settings: UserEditableSettings, settings: UserEditableSettings,
supportDynamicColor: Boolean, supportDynamicColor: Boolean,
onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit, onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit,
@ -170,7 +173,8 @@ private fun SettingsPanel(
onClick = { onChangeThemeBrand(ANDROID) }, onClick = { onChangeThemeBrand(ANDROID) },
) )
} }
if (settings.brand == DEFAULT && supportDynamicColor) { AnimatedVisibility(visible = settings.brand == DEFAULT && supportDynamicColor) {
Column {
SettingsDialogSectionTitle(text = stringResource(R.string.dynamic_color_preference)) SettingsDialogSectionTitle(text = stringResource(R.string.dynamic_color_preference))
Column(Modifier.selectableGroup()) { Column(Modifier.selectableGroup()) {
SettingsDialogThemeChooserRow( SettingsDialogThemeChooserRow(
@ -185,6 +189,7 @@ private fun SettingsPanel(
) )
} }
} }
}
SettingsDialogSectionTitle(text = stringResource(R.string.dark_mode_preference)) SettingsDialogSectionTitle(text = stringResource(R.string.dark_mode_preference))
Column(Modifier.selectableGroup()) { Column(Modifier.selectableGroup()) {
SettingsDialogThemeChooserRow( SettingsDialogThemeChooserRow(

@ -22,7 +22,7 @@ org.gradle.configureondemand=false
org.gradle.caching=true org.gradle.caching=true
# Enable configuration caching between builds. # 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 # 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 # Android operating system, and which are packaged with your app"s APK
@ -35,9 +35,6 @@ kotlin.code.style=official
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
# Disable build features that are enabled by default, # Disable build features that are enabled by default,
# https://developer.android.com/studio/releases/gradle-plugin#buildFeatures # https://developer.android.com/build/releases/gradle-plugin#default-changes
android.defaults.buildfeatures.buildconfig=false
android.defaults.buildfeatures.aidl=false
android.defaults.buildfeatures.renderscript=false
android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false android.defaults.buildfeatures.shaders=false

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

Loading…
Cancel
Save