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 {
defaultConfig {
applicationId = "com.google.samples.apps.nowinandroid"
versionCode = 5
versionName = "0.0.5" // X.Y.Z; X = Major, Y = minor, Z = Patch level
versionCode = 7
versionName = "0.1.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level
// Custom test runner to set up Hilt dependency graph
testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"

@ -183,7 +183,7 @@ class NiaAppStateTest {
@Composable
private fun rememberTestNavController(): TestNavHostController {
val context = LocalContext.current
val navController = remember {
return remember<TestNavHostController> {
TestNavHostController(context).apply {
navigatorProvider.addNavigator(ComposeNavigator())
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"))
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.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import com.google.samples.apps.nowinandroid.allowNotifications
import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent
import org.junit.Rule
import org.junit.Test
@ -86,6 +87,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
},
) {
startActivityAndWait()
allowNotifications()
// Waits until the content is ready to capture Time To Full Display
forYouWaitForContent()
}

@ -21,7 +21,7 @@
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
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"
export ANDROID_HOME="$(cd $DIR/../../../prebuilts/fullsdk/linux && pwd )"

@ -80,17 +80,14 @@ class OfflineFirstNewsRepository @Inject constructor(
val hasOnboarded = userData.shouldHideOnboarding
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 {
hasOnboarded -> newsResourceDao.getNewsResources(
hasOnboarded -> newsResourceDao.getNewsResourceIds(
useFilterTopicIds = true,
filterTopicIds = followedTopicIds,
useFilterNewsIds = true,
filterNewsIds = changedIds.toSet(),
)
.first()
.map { it.entity.id }
.toSet()
// No need to retrieve anything if notifications won't be sent
else -> emptySet()

@ -67,6 +67,33 @@ class TestNewsResourceDao : NewsResourceDao {
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(
entities: List<NewsResourceEntity>,
): List<Long> {

@ -65,6 +65,37 @@ interface NewsResourceDao {
filterNewsIds: Set<String> = emptySet(),
): 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
*/

@ -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 {
implementation(project(":core:common"))
implementation(project(":core:model"))

@ -70,6 +70,7 @@ import kotlinx.datetime.Instant
import kotlinx.datetime.toJavaInstant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
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")
.withZone(zoneId).format(publishDate.toJavaInstant())
return DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.getDefault())
.withZone(zoneId)
.format(publishDate.toJavaInstant())
}
@Composable

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

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

@ -38,3 +38,7 @@ android.nonTransitiveRClass=true
# https://developer.android.com/build/releases/gradle-plugin#default-changes
android.defaults.buildfeatures.resvalues=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"
androidxBrowser = "1.4.0"
androidxComposeBom = "2023.06.01"
androidxComposeCompiler = "1.4.8"
androidxComposeCompiler = "1.5.0"
androidxComposeRuntimeTracing = "1.0.0-alpha03"
androidxCore = "1.9.0"
androidxCoreSplashscreen = "1.0.0"
@ -34,18 +34,18 @@ firebasePerfPlugin = "1.4.2"
gmsPlugin = "4.3.14"
googleOss = "17.0.1"
googleOssPlugin = "0.10.6"
hilt = "2.46.1"
hilt = "2.47"
hiltExt = "1.0.0"
jacoco = "0.8.7"
junit4 = "4.13.2"
kotlin = "1.8.22"
kotlin = "1.9.0"
kotlinxCoroutines = "1.6.4"
kotlinxDatetime = "0.4.0"
kotlinxSerializationJson = "1.5.1"
ksp = "1.8.22-1.0.11"
lint = "30.3.1"
ksp = "1.9.0-1.0.11"
lint = "31.0.2"
okhttp = "4.10.0"
protobuf = "3.23.0"
protobuf = "3.23.4"
protobufPlugin = "0.9.3"
retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "1.0.0"

@ -36,7 +36,10 @@ echo y | ${ANDROID_HOME}/tools/bin/sdkmanager --licenses
cd $KOKORO_ARTIFACTS_DIR/git/nowinandroid
# 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
export JAVA_HOME=

@ -21,12 +21,11 @@ set -e
set -x
# 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
# gts4lltevzw | Galaxy Tab S4 | API 28 | Tablet
# a10 | Samsung A10 | API 29 | Phone
# redfin | Pixel 5e | API 30 | 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 $?

Loading…
Cancel
Save