Merge remote-tracking branch 'origin/main' into patch-2

pull/832/head
Simon Marquis 1 year ago
commit e257f2cbf4

@ -29,8 +29,8 @@ plugins {
android { android {
defaultConfig { defaultConfig {
applicationId = "com.google.samples.apps.nowinandroid" applicationId = "com.google.samples.apps.nowinandroid"
versionCode = 5 versionCode = 7
versionName = "0.0.5" // X.Y.Z; X = Major, Y = minor, Z = Patch level versionName = "0.1.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level
// Custom test runner to set up Hilt dependency graph // Custom test runner to set up Hilt dependency graph
testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"

@ -183,7 +183,7 @@ class NiaAppStateTest {
@Composable @Composable
private fun rememberTestNavController(): TestNavHostController { private fun rememberTestNavController(): TestNavHostController {
val context = LocalContext.current val context = LocalContext.current
val navController = remember { return remember<TestNavHostController> {
TestNavHostController(context).apply { TestNavHostController(context).apply {
navigatorProvider.addNavigator(ComposeNavigator()) navigatorProvider.addNavigator(ComposeNavigator())
graph = createGraph(startDestination = "a") { graph = createGraph(startDestination = "a") {
@ -193,5 +193,4 @@ private fun rememberTestNavController(): TestNavHostController {
} }
} }
} }
return navController
} }

@ -88,3 +88,17 @@ fun MacrobenchmarkScope.forYouScrollFeedDownUp() {
val feedList = device.findObject(By.res("forYou:feed")) val feedList = device.findObject(By.res("forYou:feed"))
device.flingElementDownUp(feedList) device.flingElementDownUp(feedList)
} }
fun MacrobenchmarkScope.setAppTheme(isDark: Boolean) {
when (isDark) {
true -> device.findObject(By.text("Dark")).click()
false -> device.findObject(By.text("Light")).click()
}
device.waitForIdle()
device.findObject(By.text("OK")).click()
// Wait until the top app bar is visible on screen
device.wait(Until.hasObject(By.res("niaTopAppBar")), 2_000)
val topAppBar = device.findObject(By.res("niaTopAppBar"))
topAppBar.wait(Until.hasObject(By.text("Now in Android")), 2_000)
}

@ -0,0 +1,83 @@
/*
* 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 android.os.Build.VERSION_CODES
import androidx.annotation.RequiresApi
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.PowerCategory
import androidx.benchmark.macro.PowerCategoryDisplayLevel
import androidx.benchmark.macro.PowerMetric
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 com.google.samples.apps.nowinandroid.foryou.forYouScrollFeedDownUp
import com.google.samples.apps.nowinandroid.foryou.forYouSelectTopics
import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent
import com.google.samples.apps.nowinandroid.foryou.setAppTheme
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@OptIn(ExperimentalMetricApi::class)
@RequiresApi(VERSION_CODES.Q)
@RunWith(AndroidJUnit4::class)
class ScrollTopicListPowerMetricsBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
private val categories = PowerCategory.values()
.associateWith { PowerCategoryDisplayLevel.TOTAL }
@Test
fun benchmarkStateChangeCompilationLight() =
benchmarkStateChangeWithTheme(CompilationMode.Partial(), false)
@Test
fun benchmarkStateChangeCompilationDark() =
benchmarkStateChangeWithTheme(CompilationMode.Partial(), true)
private fun benchmarkStateChangeWithTheme(compilationMode: CompilationMode, isDark: Boolean) =
benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics = listOf(FrameTimingMetric(), PowerMetric(PowerMetric.Energy(categories))),
compilationMode = compilationMode,
iterations = 2,
startupMode = StartupMode.WARM,
setupBlock = {
// Start the app
pressHome()
startActivityAndWait()
allowNotifications()
// Navigate to Settings
device.findObject(By.desc("Settings")).click()
device.waitForIdle()
setAppTheme(isDark)
},
) {
forYouWaitForContent()
forYouSelectTopics()
repeat(3) {
forYouScrollFeedDownUp()
}
}
}

@ -27,6 +27,7 @@ import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.google.samples.apps.nowinandroid.PACKAGE_NAME import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import com.google.samples.apps.nowinandroid.allowNotifications
import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -86,6 +87,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
}, },
) { ) {
startActivityAndWait() startActivityAndWait()
allowNotifications()
// Waits until the content is ready to capture Time To Full Display // Waits until the content is ready to capture Time To Full Display
forYouWaitForContent() forYouWaitForContent()
} }

@ -21,7 +21,7 @@
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
APP_OUT=$DIR/app/build/outputs APP_OUT=$DIR/app/build/outputs
export JAVA_HOME="$(cd $DIR/../../../prebuilts/studio/jdk/jdk11/linux && pwd )" export JAVA_HOME="$(cd $DIR/../nowinandroid-prebuilts/jdk17/linux && pwd )"
echo "JAVA_HOME=$JAVA_HOME" echo "JAVA_HOME=$JAVA_HOME"
export ANDROID_HOME="$(cd $DIR/../../../prebuilts/fullsdk/linux && pwd )" export ANDROID_HOME="$(cd $DIR/../../../prebuilts/fullsdk/linux && pwd )"

@ -80,17 +80,14 @@ class OfflineFirstNewsRepository @Inject constructor(
val hasOnboarded = userData.shouldHideOnboarding val hasOnboarded = userData.shouldHideOnboarding
val followedTopicIds = userData.followedTopics val followedTopicIds = userData.followedTopics
// TODO: Make this more efficient, there is no need to retrieve populated
// news resources when all that's needed are the ids
val existingNewsResourceIdsThatHaveChanged = when { val existingNewsResourceIdsThatHaveChanged = when {
hasOnboarded -> newsResourceDao.getNewsResources( hasOnboarded -> newsResourceDao.getNewsResourceIds(
useFilterTopicIds = true, useFilterTopicIds = true,
filterTopicIds = followedTopicIds, filterTopicIds = followedTopicIds,
useFilterNewsIds = true, useFilterNewsIds = true,
filterNewsIds = changedIds.toSet(), filterNewsIds = changedIds.toSet(),
) )
.first() .first()
.map { it.entity.id }
.toSet() .toSet()
// No need to retrieve anything if notifications won't be sent // No need to retrieve anything if notifications won't be sent
else -> emptySet() else -> emptySet()

@ -67,6 +67,33 @@ class TestNewsResourceDao : NewsResourceDao {
result result
} }
override fun getNewsResourceIds(
useFilterTopicIds: Boolean,
filterTopicIds: Set<String>,
useFilterNewsIds: Boolean,
filterNewsIds: Set<String>,
): Flow<List<String>> =
entitiesStateFlow
.map { newsResourceEntities ->
newsResourceEntities.map { entity ->
entity.asPopulatedNewsResource(topicCrossReferences)
}
}
.map { resources ->
var result = resources
if (useFilterTopicIds) {
result = result.filter { resource ->
resource.topics.any { it.id in filterTopicIds }
}
}
if (useFilterNewsIds) {
result = result.filter { resource ->
resource.entity.id in filterNewsIds
}
}
result.map { it.entity.id }
}
override suspend fun insertOrIgnoreNewsResources( override suspend fun insertOrIgnoreNewsResources(
entities: List<NewsResourceEntity>, entities: List<NewsResourceEntity>,
): List<Long> { ): List<Long> {

@ -65,6 +65,37 @@ interface NewsResourceDao {
filterNewsIds: Set<String> = emptySet(), filterNewsIds: Set<String> = emptySet(),
): Flow<List<PopulatedNewsResource>> ): Flow<List<PopulatedNewsResource>>
/**
* Fetches ids of news resources that match the query parameters
*/
@Transaction
@Query(
value = """
SELECT id FROM news_resources
WHERE
CASE WHEN :useFilterNewsIds
THEN id IN (:filterNewsIds)
ELSE 1
END
AND
CASE WHEN :useFilterTopicIds
THEN id IN
(
SELECT news_resource_id FROM news_resources_topics
WHERE topic_id IN (:filterTopicIds)
)
ELSE 1
END
ORDER BY publish_date DESC
""",
)
fun getNewsResourceIds(
useFilterTopicIds: Boolean = false,
filterTopicIds: Set<String> = emptySet(),
useFilterNewsIds: Boolean = false,
filterNewsIds: Set<String> = emptySet(),
): Flow<List<String>>
/** /**
* Inserts [entities] into the db if they don't exist, and ignores those that do * Inserts [entities] into the db if they don't exist, and ignores those that do
*/ */

@ -52,6 +52,13 @@ protobuf {
} }
} }
androidComponents.beforeVariants {
android.sourceSets.register(it.name) {
java.srcDir(buildDir.resolve("generated/source/proto/${it.name}/java"))
kotlin.srcDir(buildDir.resolve("generated/source/proto/${it.name}/kotlin"))
}
}
dependencies { dependencies {
implementation(project(":core:common")) implementation(project(":core:common"))
implementation(project(":core:model")) implementation(project(":core:model"))

@ -70,6 +70,7 @@ import kotlinx.datetime.Instant
import kotlinx.datetime.toJavaInstant import kotlinx.datetime.toJavaInstant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR
@ -230,8 +231,11 @@ fun dateFormatted(publishDate: Instant): String {
} }
} }
return DateTimeFormatter.ofPattern("MMM d, yyyy") return DateTimeFormatter
.withZone(zoneId).format(publishDate.toJavaInstant()) .ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.getDefault())
.withZone(zoneId)
.format(publishDate.toJavaInstant())
} }
@Composable @Composable

@ -18,22 +18,20 @@ package com.google.samples.apps.nowinandroid.feature.interests
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
@ -51,63 +49,46 @@ fun InterestsItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
iconModifier: Modifier = Modifier, iconModifier: Modifier = Modifier,
description: String = "", description: String = "",
itemSeparation: Dp = 16.dp,
) { ) {
Row( ListItem(
verticalAlignment = Alignment.CenterVertically, leadingContent = {
modifier = modifier,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(1f)
.clickable { onClick() }
.padding(vertical = itemSeparation),
) {
InterestsIcon(topicImageUrl, iconModifier.size(64.dp)) InterestsIcon(topicImageUrl, iconModifier.size(64.dp))
Spacer(modifier = Modifier.width(24.dp)) },
InterestContent(name, description) headlineContent = {
} Text(text = name)
NiaIconToggleButton( },
checked = following, supportingContent = {
onCheckedChange = onFollowButtonClick, Text(text = description)
icon = { },
Icon( trailingContent = {
imageVector = NiaIcons.Add, NiaIconToggleButton(
contentDescription = stringResource( checked = following,
id = string.card_follow_button_content_desc, onCheckedChange = onFollowButtonClick,
), icon = {
) Icon(
}, imageVector = NiaIcons.Add,
checkedIcon = { contentDescription = stringResource(
Icon( id = string.card_follow_button_content_desc,
imageVector = NiaIcons.Check, ),
contentDescription = stringResource( )
id = string.card_unfollow_button_content_desc, },
), checkedIcon = {
) Icon(
}, imageVector = NiaIcons.Check,
) contentDescription = stringResource(
} id = string.card_unfollow_button_content_desc,
} ),
)
@Composable },
private fun InterestContent(name: String, description: String, modifier: Modifier = Modifier) {
Column(modifier) {
Text(
text = name,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(
vertical = if (description.isEmpty()) 0.dp else 4.dp,
),
)
if (description.isNotEmpty()) {
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
) )
} },
} colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
),
modifier = modifier
.semantics(mergeDescendants = true) { /* no-op */ }
.clickable(enabled = true, onClick = onClick),
)
} }
@Composable @Composable

@ -20,11 +20,14 @@ import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.hasScrollToNodeAction
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollToIndex
import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID
@ -139,15 +142,18 @@ class SearchScreenTest {
composeTestRule composeTestRule
.onNodeWithText(topicsString) .onNodeWithText(topicsString)
.assertIsDisplayed() .assertIsDisplayed()
composeTestRule
.onNodeWithText(followableTopicTestData[0].topic.name) val scrollableNode = composeTestRule
.assertIsDisplayed() .onAllNodes(hasScrollToNodeAction())
composeTestRule .onFirst()
.onNodeWithText(followableTopicTestData[1].topic.name)
.assertIsDisplayed() followableTopicTestData.forEachIndexed { index, followableTopic ->
composeTestRule scrollableNode.performScrollToIndex(index)
.onNodeWithText(followableTopicTestData[2].topic.name)
.assertIsDisplayed() composeTestRule
.onNodeWithText(followableTopic.topic.name)
.assertIsDisplayed()
}
composeTestRule composeTestRule
.onAllNodesWithContentDescription(followButtonContentDesc) .onAllNodesWithContentDescription(followButtonContentDesc)

@ -38,3 +38,7 @@ android.nonTransitiveRClass=true
# https://developer.android.com/build/releases/gradle-plugin#default-changes # https://developer.android.com/build/releases/gradle-plugin#default-changes
android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false android.defaults.buildfeatures.shaders=false
# Use newer lint version to support Kotlin 1.9 and corresponding kotlinx-metadata-jvm
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
android.experimental.lint.version=8.1.0-rc01

@ -6,7 +6,7 @@ androidxActivity = "1.7.0"
androidxAppCompat = "1.5.1" androidxAppCompat = "1.5.1"
androidxBrowser = "1.4.0" androidxBrowser = "1.4.0"
androidxComposeBom = "2023.06.01" androidxComposeBom = "2023.06.01"
androidxComposeCompiler = "1.4.8" androidxComposeCompiler = "1.5.0"
androidxComposeRuntimeTracing = "1.0.0-alpha03" androidxComposeRuntimeTracing = "1.0.0-alpha03"
androidxCore = "1.9.0" androidxCore = "1.9.0"
androidxCoreSplashscreen = "1.0.0" androidxCoreSplashscreen = "1.0.0"
@ -34,18 +34,18 @@ firebasePerfPlugin = "1.4.2"
gmsPlugin = "4.3.14" gmsPlugin = "4.3.14"
googleOss = "17.0.1" googleOss = "17.0.1"
googleOssPlugin = "0.10.6" googleOssPlugin = "0.10.6"
hilt = "2.46.1" hilt = "2.47"
hiltExt = "1.0.0" hiltExt = "1.0.0"
jacoco = "0.8.7" jacoco = "0.8.7"
junit4 = "4.13.2" junit4 = "4.13.2"
kotlin = "1.8.22" kotlin = "1.9.0"
kotlinxCoroutines = "1.6.4" kotlinxCoroutines = "1.6.4"
kotlinxDatetime = "0.4.0" kotlinxDatetime = "0.4.0"
kotlinxSerializationJson = "1.5.1" kotlinxSerializationJson = "1.5.1"
ksp = "1.8.22-1.0.11" ksp = "1.9.0-1.0.11"
lint = "30.3.1" lint = "31.0.2"
okhttp = "4.10.0" okhttp = "4.10.0"
protobuf = "3.23.0" protobuf = "3.23.4"
protobufPlugin = "0.9.3" protobufPlugin = "0.9.3"
retrofit = "2.9.0" retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "1.0.0" retrofitKotlinxSerializationJson = "1.0.0"

@ -36,7 +36,10 @@ echo y | ${ANDROID_HOME}/tools/bin/sdkmanager --licenses
cd $KOKORO_ARTIFACTS_DIR/git/nowinandroid cd $KOKORO_ARTIFACTS_DIR/git/nowinandroid
# The build needs Java 17, set it as the default Java version. # The build needs Java 17, set it as the default Java version.
sudo update-java-alternatives --set java-1.17.0-openjdk-amd64 sudo apt-get update
sudo apt-get install -y openjdk-17-jdk
sudo update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java
java -version
# Also clear JAVA_HOME variable so java -version is used instead # Also clear JAVA_HOME variable so java -version is used instead
export JAVA_HOME= export JAVA_HOME=

@ -21,12 +21,11 @@ set -e
set -x set -x
# Run the normal build, but replace the default virtual devices with physical ones. # Run the normal build, but replace the default virtual devices with physical ones.
# hammerhead | Nexus 5 | API 23 | Phone
# walleye | Pixel 2 | API 27 | Phone # walleye | Pixel 2 | API 27 | Phone
# gts4lltevzw | Galaxy Tab S4 | API 28 | Tablet # gts4lltevzw | Galaxy Tab S4 | API 28 | Tablet
# a10 | Samsung A10 | API 29 | Phone # a10 | Samsung A10 | API 29 | Phone
# redfin | Pixel 5e | API 30 | Phone # redfin | Pixel 5e | API 30 | Phone
# oriole | Pixel 6 | API 31 | Phone # oriole | Pixel 6 | API 31 | Phone
bash $KOKORO_ARTIFACTS_DIR/git/nowinandroid/kokoro/build.sh "hammerhead,walleye,gts4lltevzw,a10,redfin,oriole" "23,27,28,29,30,31" bash $KOKORO_ARTIFACTS_DIR/git/nowinandroid/kokoro/build.sh "walleye,gts4lltevzw,a10,redfin,oriole" "27,28,29,30,31"
exit $? exit $?

Loading…
Cancel
Save