From ae50f3d332d5049843bc0458c743dce46fd697fc Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Wed, 24 May 2023 18:46:33 +0100 Subject: [PATCH 01/29] Remove benchmark androidTest task exclusion Task `:benchmark:connectedDemoBenchmarkAndroidTest` is not part of the `connectedDemoDebugAndroidTest` task graph and can therefore safely be removed. --- .github/workflows/Build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 0389dcf56..a3b328dcb 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -100,7 +100,7 @@ jobs: disable-animations: true disk-size: 6000M heap-size: 600M - script: ./gradlew connectedDemoDebugAndroidTest -x :benchmark:connectedDemoBenchmarkAndroidTest --daemon + script: ./gradlew connectedDemoDebugAndroidTest --daemon - name: Upload test reports if: always() From 823c4db2013a27616b9d88aaca3349d87c13160d Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Wed, 24 May 2023 20:11:36 +0200 Subject: [PATCH 02/29] Grant `POST_NOTIFICATIONS` permission in more instrumented tests Continues the work initiated in #738. Extract the SDK version check inside a `GrantPostNotificationPermissionRule` class that delegates to a regular `GrantPermissionRule`. --- .../apps/nowinandroid/ui/NavigationTest.kt | 9 +++++- .../apps/nowinandroid/ui/NavigationUiTest.kt | 9 +++++- .../GrantPostNotificationsPermissionRule.kt | 29 +++++++++++++++++++ .../feature/foryou/ForYouScreenTest.kt | 17 +++-------- 4 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/rules/GrantPostNotificationsPermissionRule.kt diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index 5aa3ab02e..036a2955c 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -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() private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt index cd4b40a50..d92390918 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt @@ -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() val userNewsResourceRepository = CompositeUserNewsResourceRepository( diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/rules/GrantPostNotificationsPermissionRule.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/rules/GrantPostNotificationsPermissionRule.kt new file mode 100644 index 000000000..512399d85 --- /dev/null +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/rules/GrantPostNotificationsPermissionRule.kt @@ -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() diff --git a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt index b138cba06..8dcdef6be 100644 --- a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt +++ b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt @@ -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() private val doneButtonMatcher by lazy { From cd6ad7d575a82c6a88c9c8e0e13841743d3031e7 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Thu, 25 May 2023 08:32:26 +0100 Subject: [PATCH 03/29] Ensure DAO exposes only reactive types Change-Id: I8992ccf6525cd4c5ea9503d532b500e4d310b7fb --- .../data/repository/DefaultSearchContentsRepository.kt | 9 ++++++++- .../nowinandroid/core/database/dao/NewsResourceDao.kt | 4 ---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt index 40b170cbe..dc3caa143 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt @@ -20,6 +20,7 @@ import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao import com.google.samples.apps.nowinandroid.core.database.dao.TopicFtsDao +import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsResource import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import com.google.samples.apps.nowinandroid.core.database.model.asFtsEntity import com.google.samples.apps.nowinandroid.core.model.data.SearchResult @@ -29,6 +30,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext @@ -45,7 +47,12 @@ class DefaultSearchContentsRepository @Inject constructor( override suspend fun populateFtsData() { withContext(ioDispatcher) { newsResourceFtsDao.insertAll( - newsResourceDao.getOneOffNewsResources().map { it.asFtsEntity() }, + newsResourceDao.getNewsResources( + useFilterTopicIds = false, + useFilterNewsIds = false, + ) + .first() + .map(PopulatedNewsResource::asFtsEntity), ) topicFtsDao.insertAll(topicDao.getOneOffTopicEntities().map { it.asFtsEntity() }) } diff --git a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt index b5949c6d2..a05507a8b 100644 --- a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt +++ b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt @@ -65,10 +65,6 @@ interface NewsResourceDao { filterNewsIds: Set = emptySet(), ): Flow> - @Transaction - @Query(value = "SELECT * FROM news_resources ORDER BY publish_date DESC") - suspend fun getOneOffNewsResources(): List - /** * Inserts [entities] into the db if they don't exist, and ignores those that do */ From 4cdb5963a0af0ee50213b9238130f39d15d3ffba Mon Sep 17 00:00:00 2001 From: Don Turner Date: Thu, 25 May 2023 10:00:55 +0100 Subject: [PATCH 04/29] Add top level application scope, use IO dispatcher for DataStore Change-Id: I1512b1665587c73abd5e8a78aa9abd3eed24ab79 --- .../core/network/di/CoroutineScopesModule.kt | 15 ++++++++++----- .../core/datastore/di/DataStoreModule.kt | 7 +++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/CoroutineScopesModule.kt b/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/CoroutineScopesModule.kt index b73c63568..c265394a8 100644 --- a/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/CoroutineScopesModule.kt +++ b/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/CoroutineScopesModule.kt @@ -17,7 +17,7 @@ package com.google.samples.apps.nowinandroid.core.network.di import com.google.samples.apps.nowinandroid.core.network.Dispatcher -import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -25,15 +25,20 @@ import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import javax.inject.Qualifier import javax.inject.Singleton +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class ApplicationScope + @Module @InstallIn(SingletonComponent::class) object CoroutineScopesModule { @Provides @Singleton - @Dispatcher(IO) - fun providesIOCoroutineScope( - @Dispatcher(IO) ioDispatcher: CoroutineDispatcher, - ): CoroutineScope = CoroutineScope(SupervisorJob() + ioDispatcher) + @ApplicationScope + fun providesCoroutineScope( + @Dispatcher(Default) dispatcher: CoroutineDispatcher, + ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) } diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt index 89a6eb734..ac9eaf767 100644 --- a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt +++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt @@ -25,11 +25,13 @@ import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO +import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import javax.inject.Singleton @@ -41,12 +43,13 @@ object DataStoreModule { @Singleton fun providesUserPreferencesDataStore( @ApplicationContext context: Context, - @Dispatcher(IO) ioScope: CoroutineScope, + @Dispatcher(IO) ioDispatcher: CoroutineDispatcher, + @ApplicationScope scope: CoroutineScope, userPreferencesSerializer: UserPreferencesSerializer, ): DataStore = DataStoreFactory.create( serializer = userPreferencesSerializer, - scope = ioScope, + scope = CoroutineScope(scope.coroutineContext + ioDispatcher), migrations = listOf( IntToStringIdsMigration, ), From dcc36b0228cc0e99dda9983a661e2879bcd21e68 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Thu, 25 May 2023 11:46:59 +0100 Subject: [PATCH 05/29] Fix tests Change-Id: Ie6b62071d2023e3a7ffe1553ab606e3f6b386a5e --- .../nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt index 09af77213..d5d8932e7 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt @@ -67,8 +67,6 @@ class TestNewsResourceDao : NewsResourceDao { result } - override suspend fun getOneOffNewsResources(): List = emptyList() - override suspend fun insertOrIgnoreNewsResources( entities: List, ): List { From b245334a1811f89141bcacb5060fb2bcf2a48e8e Mon Sep 17 00:00:00 2001 From: Don Turner Date: Thu, 25 May 2023 13:11:14 +0100 Subject: [PATCH 06/29] Updating TestDataStoreModule to use ApplicationScope --- .../core/datastore/test/TestDataStoreModule.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/datastore-test/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt b/core/datastore-test/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt index cb7e38db7..b86003e83 100644 --- a/core/datastore-test/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt +++ b/core/datastore-test/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt @@ -21,8 +21,7 @@ import androidx.datastore.core.DataStoreFactory import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer import com.google.samples.apps.nowinandroid.core.datastore.di.DataStoreModule -import com.google.samples.apps.nowinandroid.core.network.Dispatcher -import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO +import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope import dagger.Module import dagger.Provides import dagger.hilt.components.SingletonComponent @@ -41,12 +40,12 @@ object TestDataStoreModule { @Provides @Singleton fun providesUserPreferencesDataStore( - @Dispatcher(IO) ioScope: CoroutineScope, + @ApplicationScope scope: CoroutineScope, userPreferencesSerializer: UserPreferencesSerializer, tmpFolder: TemporaryFolder, ): DataStore = tmpFolder.testUserPreferencesDataStore( - coroutineScope = ioScope, + coroutineScope = scope, userPreferencesSerializer = userPreferencesSerializer, ) } From 49c8ce700e6cfb6447e97421d7b2492a4cbdbac2 Mon Sep 17 00:00:00 2001 From: Alejandra Stamato Date: Wed, 17 May 2023 18:28:31 +0100 Subject: [PATCH 07/29] Fixing ScrollForYouFeedBenchmark Co-authored-by: Simon Marquis --- .../apps/nowinandroid/GeneralActions.kt | 44 +++++++++++++++++++ .../foryou/ScrollForYouFeedBenchmark.kt | 2 + .../TopicsScreenRecompositionBenchmark.kt | 3 +- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 benchmarks/src/main/java/com/google/samples/apps/nowinandroid/GeneralActions.kt diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/GeneralActions.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/GeneralActions.kt new file mode 100644 index 000000000..48472e523 --- /dev/null +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/GeneralActions.kt @@ -0,0 +1,44 @@ +/* + * 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 android.Manifest.permission +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES.TIRAMISU +import androidx.benchmark.macro.MacrobenchmarkScope + +/** + * Because the app under test is different from the one running the instrumentation test, + * the permission has to be granted manually by either: + * + * - tapping the Allow button + * ```kotlin + * val obj = By.text("Allow") + * val dialog = device.wait(Until.findObject(obj), TIMEOUT) + * dialog?.let { + * it.click() + * device.wait(Until.gone(obj), 5_000) + * } + * ``` + * - or (preferred) executing the grant command on the target package. + */ +fun MacrobenchmarkScope.allowNotifications() { + if (SDK_INT >= TIRAMISU) { + val command = "pm grant $packageName ${permission.POST_NOTIFICATIONS}" + device.executeShellCommand(command) + } +} diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt index f8945a31c..3008fdc0d 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt @@ -22,6 +22,7 @@ import androidx.benchmark.macro.StartupMode 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 org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -47,6 +48,7 @@ class ScrollForYouFeedBenchmark { // Start the app pressHome() startActivityAndWait() + allowNotifications() }, ) { forYouWaitForContent() diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/TopicsScreenRecompositionBenchmark.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/TopicsScreenRecompositionBenchmark.kt index 24bd233ea..0030386b7 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/TopicsScreenRecompositionBenchmark.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/TopicsScreenRecompositionBenchmark.kt @@ -23,6 +23,7 @@ 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 @@ -47,7 +48,7 @@ class TopicsScreenRecompositionBenchmark { // Start the app pressHome() startActivityAndWait() - + allowNotifications() // Navigate to interests screen device.findObject(By.text("Interests")).click() device.waitForIdle() From c64cf13b4ecae1af81bb3657245fb9b78d028fa1 Mon Sep 17 00:00:00 2001 From: Alejandra Stamato Date: Thu, 25 May 2023 16:10:40 +0100 Subject: [PATCH 08/29] Added Compose compiler metrics generation command to README.md --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cd699caa4..dec8ec961 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,14 @@ The app uses adaptive layouts to Find out more about the [UI architecture here](docs/ArchitectureLearningJourney.md#ui-layer). -# Baseline profiles +# Performance + +## Benchmarks + +Find all tests written using [`Macrobenchmark`](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview) +in the `benchmarks` module. This module also contains the test to generate the Baseline profile. + +## Baseline profiles The baseline profile for this app is located at [`app/src/main/baseline-prof.txt`](app/src/main/baseline-prof.txt). It contains rules that enable AOT compilation of the critical user path taken during app launch. @@ -144,6 +151,16 @@ To generate the baseline profile, select the `benchmark` build variant and run t `BaselineProfileGenerator` benchmark test on an AOSP Android Emulator. Then copy the resulting baseline profile from the emulator to [`app/src/main/baseline-prof.txt`](app/src/main/baseline-prof.txt). +## Compose compiler metrics + +Run the following command to get and analyse compose compiler metrics: + +``` +./gradlew assembleRelease -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true +``` + +For more information on Compose compiler metrics, see [this blog post](https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8). + # License **Now in Android** is distributed under the terms of the Apache License (Version 2.0). See the From 96c7ada651cd22be6b5250cd1784eda664267cef Mon Sep 17 00:00:00 2001 From: Alejandra Stamato Date: Thu, 25 May 2023 16:21:13 +0100 Subject: [PATCH 09/29] Added Compose compiler metrics generation command to README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index dec8ec961..9aca22cbd 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,9 @@ Run the following command to get and analyse compose compiler metrics: ./gradlew assembleRelease -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true ``` +The reports files will be added to build/compose-reports in each module. The metrics files will be +added to build/compose-metrics in each module. + For more information on Compose compiler metrics, see [this blog post](https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8). # License From d319264ef061dd52dbbcc3b739d670eeb7d3d4df Mon Sep 17 00:00:00 2001 From: Don Turner Date: Thu, 25 May 2023 17:08:44 +0100 Subject: [PATCH 10/29] Provide Default dispatcher for instrumented tests Change-Id: I488a9b9d9d3864ce9496614ab35332ec19d06bcc --- core/data-test/build.gradle.kts | 1 + core/datastore-test/build.gradle.kts | 1 + .../nowinandroid/core/testing/di/TestDispatchersModule.kt | 7 +++++++ 3 files changed, 9 insertions(+) diff --git a/core/data-test/build.gradle.kts b/core/data-test/build.gradle.kts index f50e7b4b8..dfc224e19 100644 --- a/core/data-test/build.gradle.kts +++ b/core/data-test/build.gradle.kts @@ -25,4 +25,5 @@ android { dependencies { api(project(":core:data")) implementation(project(":core:testing")) + implementation(project(":core:common")) } diff --git a/core/datastore-test/build.gradle.kts b/core/datastore-test/build.gradle.kts index c7c423c25..193c49da7 100644 --- a/core/datastore-test/build.gradle.kts +++ b/core/datastore-test/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { api(project(":core:datastore")) api(libs.androidx.dataStore.core) + implementation(libs.protobuf.kotlin.lite) implementation(project(":core:common")) implementation(project(":core:testing")) } diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt index a5eb506ae..f2134105a 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.core.testing.di import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.di.DispatchersModule import dagger.Module @@ -35,4 +36,10 @@ object TestDispatchersModule { @Provides @Dispatcher(IO) fun providesIODispatcher(testDispatcher: TestDispatcher): CoroutineDispatcher = testDispatcher + + @Provides + @Dispatcher(Default) + fun providesDefaultDispatcher( + testDispatcher: TestDispatcher, + ): CoroutineDispatcher = testDispatcher } From 8c8c7611ce9461ca0df994d5ec81fcd18b3b2ee0 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 27 May 2023 12:07:36 +0200 Subject: [PATCH 11/29] Update KotlinX Serialization to version 1.5.1 https://github.com/Kotlin/kotlinx.serialization/releases/tag/v1.5.1 > ### Bugfixes > - KeyValueSerializer: Fix missing call to endStructure() (#2272) > - ObjectSerializer: Respect sequential decoding (#2273) > - Fix value class encoding in various corner cases (#2242) > - Fix incorrect json decoding iterator's .hasNext() behavior on array-wrapped inputs (#2268) > - Fix memory leak caused by invalid KTypeWrapper's equals method (#2274) > - Fixed NoSuchMethodError when parsing a JSON stream on Java 8 (#2219) > - Fix MissingFieldException duplication (#2213) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0be000440..f2d071b56 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" From 502f9ed1108ed35344bafe128b5b88e99f4a7b17 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 27 May 2023 13:07:58 +0100 Subject: [PATCH 12/29] Remove `ExperimentalCoroutinesApi` and `FlowPreview` opt-in ``` > Task :core:model:compileKotlin w: Opt-in requirement marker kotlinx.coroutines.ExperimentalCoroutinesApi is unresolved. Please make sure it's present in the module dependencies w: Opt-in requirement marker kotlinx.coroutines.FlowPreview is unresolved. Please make sure it's present in the module dependencies ``` --- .../com/google/samples/apps/nowinandroid/KotlinAndroid.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt index 43edd53ec..bf0bc5e72 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt @@ -92,9 +92,6 @@ 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", ) } } From 97a55c9dd52168a6c352e19e4cdbcff1b860a31f Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 27 May 2023 20:51:41 +0200 Subject: [PATCH 13/29] Merge `AndroidCIWithGmd.yaml` into `Build.yaml` Add dependency on the `build` job, and add the same timeout of 55 minutes. Closes #761 --- .github/workflows/AndroidCIWithGmd.yaml | 49 ------------------------- .github/workflows/Build.yaml | 40 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 49 deletions(-) delete mode 100644 .github/workflows/AndroidCIWithGmd.yaml diff --git a/.github/workflows/AndroidCIWithGmd.yaml b/.github/workflows/AndroidCIWithGmd.yaml deleted file mode 100644 index e10c49f9e..000000000 --- a/.github/workflows/AndroidCIWithGmd.yaml +++ /dev/null @@ -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' diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index a3b328dcb..f28a9a89c 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -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: 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' From b4533863274cc3510861e9867781d22b3ca7752d Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sun, 28 May 2023 12:15:36 +0200 Subject: [PATCH 14/29] Ellipsis string can be replaced with ellipsis character MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lint: Replace `...` with ellipsis character `…` --- feature/settings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index cbd4df8ed..ad56f6b08 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -18,7 +18,7 @@ Settings Search Settings - Loading... + Loading… Privacy policy Licenses Brand Guidelines From 9aba5cc0fe43f2e7459cfdec3f71527367ccf98d Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sun, 28 May 2023 12:09:20 +0200 Subject: [PATCH 15/29] Cleanup unused string resources --- core/notifications/src/main/res/values/strings.xml | 1 - feature/bookmarks/src/main/res/values/strings.xml | 3 --- feature/foryou/src/main/res/values/strings.xml | 8 -------- feature/interests/src/main/res/values/strings.xml | 3 --- feature/topic/src/main/res/values/strings.xml | 1 - 5 files changed, 16 deletions(-) diff --git a/core/notifications/src/main/res/values/strings.xml b/core/notifications/src/main/res/values/strings.xml index a3f8a4e61..5bb37b23a 100644 --- a/core/notifications/src/main/res/values/strings.xml +++ b/core/notifications/src/main/res/values/strings.xml @@ -15,7 +15,6 @@ limitations under the License. --> - Now in Android News updates The latest updates on what\'s new in Android %1$d news updates diff --git a/feature/bookmarks/src/main/res/values/strings.xml b/feature/bookmarks/src/main/res/values/strings.xml index 2dd36659e..875a90a0b 100644 --- a/feature/bookmarks/src/main/res/values/strings.xml +++ b/feature/bookmarks/src/main/res/values/strings.xml @@ -17,9 +17,6 @@ Saved Loading saved… - Saved - Search - Menu No saved updates Updates you save will be stored here\nto read later Bookmark removed diff --git a/feature/foryou/src/main/res/values/strings.xml b/feature/foryou/src/main/res/values/strings.xml index 1880ab953..5a33bc9c8 100644 --- a/feature/foryou/src/main/res/values/strings.xml +++ b/feature/foryou/src/main/res/values/strings.xml @@ -21,13 +21,5 @@ Navigate up What are you interested in? Updates from topics you follow will appear here. Follow some things to get started. - Now in Android - Search - - - You are following - You are not following - Follow - Unfollow diff --git a/feature/interests/src/main/res/values/strings.xml b/feature/interests/src/main/res/values/strings.xml index 68deb933e..384cb1deb 100644 --- a/feature/interests/src/main/res/values/strings.xml +++ b/feature/interests/src/main/res/values/strings.xml @@ -20,7 +20,4 @@ "No available data" Follow interest Unfollow interest - Interests - Menu - Search diff --git a/feature/topic/src/main/res/values/strings.xml b/feature/topic/src/main/res/values/strings.xml index 21e3ec246..284f2f7b2 100644 --- a/feature/topic/src/main/res/values/strings.xml +++ b/feature/topic/src/main/res/values/strings.xml @@ -15,6 +15,5 @@ limitations under the License. --> - Topic Loading topic From ded8b109b923feeafcbc602055a9db9e1c619d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Moczkowski?= Date: Mon, 29 May 2023 12:29:50 +0200 Subject: [PATCH 16/29] Refactor custom notificationDot modifier Change-Id: Ife24492a495b111f111a8bd16f21b7ebd469e5aa --- .../samples/apps/nowinandroid/ui/NiaApp.kt | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 6f6ab0603..de321db52 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -15,6 +15,7 @@ */ package com.google.samples.apps.nowinandroid.ui + import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row @@ -46,6 +47,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -129,6 +131,8 @@ fun NiaApp( ) } + val unreadDestinations by appState.topLevelDestinationsWithUnreadResources.collectAsStateWithLifecycle() + Scaffold( modifier = Modifier.semantics { testTagsAsResourceId = true @@ -139,7 +143,6 @@ fun NiaApp( snackbarHost = { SnackbarHost(snackbarHostState) }, bottomBar = { if (appState.shouldShowBottomBar) { - val unreadDestinations by appState.topLevelDestinationsWithUnreadResources.collectAsStateWithLifecycle() NiaBottomBar( destinations = appState.topLevelDestinations, destinationsWithUnreadResources = unreadDestinations, @@ -164,6 +167,7 @@ fun NiaApp( if (appState.shouldShowNavRail) { NiaNavRail( destinations = appState.topLevelDestinations, + destinationsWithUnreadResources = unreadDestinations, onNavigateToDestination = appState::navigateToTopLevelDestination, currentDestination = appState.currentDestination, modifier = Modifier @@ -208,6 +212,7 @@ fun NiaApp( @Composable private fun NiaNavRail( destinations: List, + destinationsWithUnreadResources: Set, onNavigateToDestination: (TopLevelDestination) -> Unit, currentDestination: NavDestination?, modifier: Modifier = Modifier, @@ -215,6 +220,7 @@ private fun NiaNavRail( NiaNavigationRail(modifier = modifier) { destinations.forEach { destination -> val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) + val hasUnread = destinationsWithUnreadResources.contains(destination) NiaNavigationRailItem( selected = selected, onClick = { onNavigateToDestination(destination) }, @@ -237,7 +243,7 @@ private fun NiaNavRail( } }, label = { Text(stringResource(destination.iconTextId)) }, - + modifier = if (hasUnread) Modifier.notificationDot() else Modifier, ) } } @@ -279,30 +285,30 @@ private fun NiaBottomBar( } }, label = { Text(stringResource(destination.iconTextId)) }, - modifier = if (hasUnread) notificationDot() else Modifier, + modifier = if (hasUnread) Modifier.notificationDot() else Modifier, ) } } } -@Composable -private fun notificationDot(): Modifier { - val tertiaryColor = MaterialTheme.colorScheme.tertiary - return Modifier.drawWithContent { - drawContent() - drawCircle( - tertiaryColor, - radius = 5.dp.toPx(), - // This is based on the dimensions of the NavigationBar's "indicator pill"; - // however, its parameters are private, so we must depend on them implicitly - // (NavigationBarTokens.ActiveIndicatorWidth = 64.dp) - center = center + Offset( - 64.dp.toPx() * .45f, - 32.dp.toPx() * -.45f - 6.dp.toPx(), - ), - ) +private fun Modifier.notificationDot(): Modifier = + composed { + val tertiaryColor = MaterialTheme.colorScheme.tertiary + drawWithContent { + drawContent() + drawCircle( + tertiaryColor, + radius = 5.dp.toPx(), + // This is based on the dimensions of the NavigationBar's "indicator pill"; + // however, its parameters are private, so we must depend on them implicitly + // (NavigationBarTokens.ActiveIndicatorWidth = 64.dp) + center = center + Offset( + 64.dp.toPx() * .45f, + 32.dp.toPx() * -.45f - 6.dp.toPx(), + ), + ) + } } -} private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) = this?.hierarchy?.any { From 5bf66739bd043c61980d588a6f264852227d7a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Moczkowski?= Date: Mon, 29 May 2023 15:21:53 +0200 Subject: [PATCH 17/29] Remove redundant statements Change-Id: Iaa25b7ba033d5e9c85cc2dc433197ed7b8b6cb89 --- benchmarks/build.gradle.kts | 1 - .../baselineprofile/BaselineProfileGenerator.kt | 2 -- .../nowinandroid/bookmarks/BookmarksActions.kt | 6 ------ .../samples/apps/nowinandroid/KotlinAndroid.kt | 4 ---- .../samples/apps/nowinandroid/NiaFlavor.kt | 7 +++---- .../core/analytics/AnalyticsEvent.kt | 2 -- .../core/database/model/NewsResourceFtsEntity.kt | 6 ------ .../core/datastore/NiaPreferencesDataSource.kt | 10 +++++----- .../core/designsystem/icon/NiaIcons.kt | 12 ------------ .../core/designsystem/theme/Color.kt | 3 --- .../testing/repository/TestUserDataRepository.kt | 16 ---------------- .../nowinandroid/core/ui/NewsResourceCard.kt | 8 -------- feature/foryou/build.gradle.kts | 2 -- .../feature/foryou/ForYouScreenTest.kt | 2 +- .../feature/foryou/ForYouViewModelTest.kt | 2 -- feature/interests/build.gradle.kts | 2 -- feature/settings/build.gradle.kts | 2 -- feature/topic/build.gradle.kts | 2 -- .../nowinandroid/feature/topic/TopicScreen.kt | 3 +-- 19 files changed, 10 insertions(+), 82 deletions(-) diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 320d66647..48a6687e4 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import com.android.build.api.dsl.ManagedVirtualDevice import com.google.samples.apps.nowinandroid.NiaBuildType import com.google.samples.apps.nowinandroid.configureFlavors diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/baselineprofile/BaselineProfileGenerator.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/baselineprofile/BaselineProfileGenerator.kt index 3dfafd647..5abf7db4a 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/baselineprofile/BaselineProfileGenerator.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/baselineprofile/BaselineProfileGenerator.kt @@ -52,8 +52,6 @@ class BaselineProfileGenerator { // Navigate to saved screen goToBookmarksScreen() - // TODO: we need to implement adding stuff to bookmarks before able to scroll it - // bookmarksScrollFeedDownUp() // Navigate to interests screen goToInterestsScreen() diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/bookmarks/BookmarksActions.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/bookmarks/BookmarksActions.kt index 3dce5b313..f66fa27a2 100644 --- a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/bookmarks/BookmarksActions.kt +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/bookmarks/BookmarksActions.kt @@ -19,7 +19,6 @@ package com.google.samples.apps.nowinandroid.bookmarks import androidx.benchmark.macro.MacrobenchmarkScope import androidx.test.uiautomator.By import androidx.test.uiautomator.Until -import com.google.samples.apps.nowinandroid.flingElementDownUp fun MacrobenchmarkScope.goToBookmarksScreen() { device.findObject(By.text("Saved")).click() @@ -29,8 +28,3 @@ fun MacrobenchmarkScope.goToBookmarksScreen() { val topAppBar = device.findObject(By.res("niaTopAppBar")) topAppBar.wait(Until.hasObject(By.text("Saved")), 2_000) } - -fun MacrobenchmarkScope.bookmarksScrollFeedDownUp() { - val feedList = device.findObject(By.res("bookmarks:feed")) - device.flingElementDownUp(feedList) -} diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt index 43edd53ec..976183aa4 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt @@ -16,20 +16,16 @@ package com.google.samples.apps.nowinandroid -import com.android.build.api.dsl.ApplicationExtension 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.ExtensionAware 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.dsl.KotlinAndroidProjectExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions import org.jetbrains.kotlin.gradle.tasks.KotlinCompile /** diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt index dec592542..ef55024e2 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt @@ -4,7 +4,6 @@ import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationProductFlavor import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.ProductFlavor -import org.gradle.api.Project @Suppress("EnumEntryName") enum class FlavorDimension { @@ -17,10 +16,10 @@ enum class FlavorDimension { @Suppress("EnumEntryName") enum class NiaFlavor(val dimension: FlavorDimension, val applicationIdSuffix: String? = null) { demo(FlavorDimension.contentType, applicationIdSuffix = ".demo"), - prod(FlavorDimension.contentType, ) + prod(FlavorDimension.contentType) } -fun Project.configureFlavors( +fun configureFlavors( commonExtension: CommonExtension<*, *, *, *>, flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {} ) { @@ -33,7 +32,7 @@ fun Project.configureFlavors( flavorConfigurationBlock(this, it) if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) { if (it.applicationIdSuffix != null) { - this.applicationIdSuffix = it.applicationIdSuffix + applicationIdSuffix = it.applicationIdSuffix } } } diff --git a/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsEvent.kt b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsEvent.kt index 3e0650eed..97ae76b56 100644 --- a/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsEvent.kt +++ b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsEvent.kt @@ -34,7 +34,6 @@ data class AnalyticsEvent( class Types { companion object { const val SCREEN_VIEW = "screen_view" // (extras: SCREEN_NAME) - const val VIEW_SEARCH_RESULTS = "view_search_results" // (extras: SEARCH_TERM) } } @@ -54,7 +53,6 @@ data class AnalyticsEvent( class ParamKeys { companion object { const val SCREEN_NAME = "screen_name" - const val SEARCH_TERM = "search_term" } } } diff --git a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt index 0ef9333c1..0ba625024 100644 --- a/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt +++ b/core/database/src/main/java/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt @@ -36,9 +36,3 @@ data class NewsResourceFtsEntity( @ColumnInfo(name = "content") val content: String, ) - -fun NewsResourceEntity.asFtsEntity() = NewsResourceFtsEntity( - newsResourceId = id, - title = title, - content = content, -) diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt index 6d585ebd4..6e2be2808 100644 --- a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt @@ -143,13 +143,13 @@ class NiaPreferencesDataSource @Inject constructor( } suspend fun setNewsResourcesViewed(newsResourceIds: List, viewed: Boolean) { - userPreferences.updateData { - it.copy { - newsResourceIds.forEach { + userPreferences.updateData { prefs -> + prefs.copy { + newsResourceIds.forEach { id -> if (viewed) { - viewedNewsResourceIds.put(it, true) + viewedNewsResourceIds.put(id, true) } else { - viewedNewsResourceIds.remove(it) + viewedNewsResourceIds.remove(id) } } } diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt index 5646f088a..ac31fbcdb 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt @@ -18,19 +18,13 @@ package com.google.samples.apps.nowinandroid.core.designsystem.icon import androidx.annotation.DrawableRes import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.ArrowDropUp import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.ExpandLess -import androidx.compose.material.icons.rounded.Fullscreen import androidx.compose.material.icons.rounded.Grid3x3 import androidx.compose.material.icons.rounded.Person -import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.ShortText @@ -45,25 +39,19 @@ import com.google.samples.apps.nowinandroid.core.designsystem.R * Now in Android icons. Material icons are [ImageVector]s, custom icons are drawable resource IDs. */ object NiaIcons { - val AccountCircle = Icons.Outlined.AccountCircle val Add = Icons.Rounded.Add val ArrowBack = Icons.Rounded.ArrowBack - val ArrowDropDown = Icons.Default.ArrowDropDown - val ArrowDropUp = Icons.Default.ArrowDropUp val Bookmark = R.drawable.ic_bookmark val BookmarkBorder = R.drawable.ic_bookmark_border val Bookmarks = R.drawable.ic_bookmarks val BookmarksBorder = R.drawable.ic_bookmarks_border val Check = Icons.Rounded.Check val Close = Icons.Rounded.Close - val ExpandLess = Icons.Rounded.ExpandLess - val Fullscreen = Icons.Rounded.Fullscreen val Grid3x3 = Icons.Rounded.Grid3x3 val MenuBook = R.drawable.ic_menu_book val MenuBookBorder = R.drawable.ic_menu_book_border val MoreVert = Icons.Default.MoreVert val Person = Icons.Rounded.Person - val PlayArrow = Icons.Rounded.PlayArrow val Search = Icons.Rounded.Search val Settings = Icons.Rounded.Settings val ShortText = Icons.Rounded.ShortText diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Color.kt index ec4fa76b7..103457b08 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/theme/Color.kt @@ -27,7 +27,6 @@ internal val Blue30 = Color(0xFF004D61) internal val Blue40 = Color(0xFF006780) internal val Blue80 = Color(0xFF5DD5FC) internal val Blue90 = Color(0xFFB8EAFF) -internal val Blue95 = Color(0xFFDDF4FF) internal val DarkGreen10 = Color(0xFF0D1F12) internal val DarkGreen20 = Color(0xFF223526) internal val DarkGreen30 = Color(0xFF394B3C) @@ -61,14 +60,12 @@ internal val Orange30 = Color(0xFF812800) internal val Orange40 = Color(0xFFA23F16) internal val Orange80 = Color(0xFFFFB59B) internal val Orange90 = Color(0xFFFFDBCF) -internal val Orange95 = Color(0xFFFFEDE8) internal val Purple10 = Color(0xFF36003C) internal val Purple20 = Color(0xFF560A5D) internal val Purple30 = Color(0xFF702776) internal val Purple40 = Color(0xFF8B418F) internal val Purple80 = Color(0xFFFFA9FE) internal val Purple90 = Color(0xFFFFD6FA) -internal val Purple95 = Color(0xFFFFEBFA) internal val PurpleGray30 = Color(0xFF4D444C) internal val PurpleGray50 = Color(0xFF7F747C) internal val PurpleGray60 = Color(0xFF998D96) diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt index 66ac80868..9d1650c98 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt @@ -112,22 +112,6 @@ class TestUserDataRepository : UserDataRepository { } } - /** - * A test-only API to allow setting/unsetting of bookmarks. - * - */ - fun setNewsResourceBookmarks(newsResourceIds: Set) { - currentUserData.let { current -> - _userData.tryEmit(current.copy(bookmarkedNewsResources = newsResourceIds)) - } - } - - /** - * A test-only API to allow querying the current followed topics. - */ - fun getCurrentFollowedTopics(): Set? = - _userData.replayCache.firstOrNull()?.followedTopics - /** * A test-only API to allow setting of user data directly. */ diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index a6a7aafc9..46521a320 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -250,14 +250,6 @@ fun NewsResourceMetaData( ) } -@Composable -fun NewsResourceLink( - @Suppress("UNUSED_PARAMETER") - newsResource: NewsResource, -) { - TODO() -} - @Composable fun NewsResourceShortDescription( newsResourceShortDescription: String, diff --git a/feature/foryou/build.gradle.kts b/feature/foryou/build.gradle.kts index 6cd5216d6..bd633e3d2 100644 --- a/feature/foryou/build.gradle.kts +++ b/feature/foryou/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -import com.android.build.api.dsl.ManagedVirtualDevice - plugins { id("nowinandroid.android.feature") id("nowinandroid.android.library.compose") diff --git a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt index b138cba06..38961a918 100644 --- a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt +++ b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt @@ -112,7 +112,7 @@ class ForYouScreenTest { @Test fun topicSelector_whenNoTopicsSelected_showsTopicChipsAndDisabledDoneButton() { - val testData = followableTopicTestData.map { it -> it.copy(isFollowed = false) } + val testData = followableTopicTestData.map { it.copy(isFollowed = false) } composeTestRule.setContent { BoxWithConstraints { diff --git a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt index e99cfb74d..6a2ea4a02 100644 --- a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt @@ -30,7 +30,6 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRe import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule -import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncManager import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.feature.foryou.navigation.LINKED_NEWS_RESOURCE_ID @@ -54,7 +53,6 @@ class ForYouViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() - private val networkMonitor = TestNetworkMonitor() private val syncManager = TestSyncManager() private val userDataRepository = TestUserDataRepository() private val topicsRepository = TestTopicsRepository() diff --git a/feature/interests/build.gradle.kts b/feature/interests/build.gradle.kts index 12b3074e4..5c4b0360a 100644 --- a/feature/interests/build.gradle.kts +++ b/feature/interests/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -import com.android.build.api.dsl.ManagedVirtualDevice - plugins { id("nowinandroid.android.feature") id("nowinandroid.android.library.compose") diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 3229c350f..ef367d612 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -import com.android.build.api.dsl.ManagedVirtualDevice - plugins { id("nowinandroid.android.feature") id("nowinandroid.android.library.compose") diff --git a/feature/topic/build.gradle.kts b/feature/topic/build.gradle.kts index 6bacd8343..ecb0630ce 100644 --- a/feature/topic/build.gradle.kts +++ b/feature/topic/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -import com.android.build.api.dsl.ManagedVirtualDevice - plugins { id("nowinandroid.android.feature") id("nowinandroid.android.library.compose") diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index fd408f9cf..b987a2752 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -59,7 +59,6 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.userNewsResourceCardItems import com.google.samples.apps.nowinandroid.feature.topic.R.string -import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading @Composable internal fun TopicRoute( @@ -107,7 +106,7 @@ internal fun TopicScreen( Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) } when (topicUiState) { - Loading -> item { + TopicUiState.Loading -> item { NiaLoadingWheel( modifier = modifier, contentDesc = stringResource(id = string.topic_loading), From 99094199bc14800d12b559fd6d1cac75e179f343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Moczkowski?= Date: Mon, 29 May 2023 16:08:44 +0200 Subject: [PATCH 18/29] Remove redundant suppressions Change-Id: Iae08b6f284cf7a387922fdec34c8a0aa0da809ef --- .../nowinandroid/core/datastore/UserPreferencesSerializer.kt | 2 -- .../apps/nowinandroid/feature/settings/SettingsDialog.kt | 1 - .../apps/nowinandroid/lint/designsystem/DesignSystemDetector.kt | 1 - .../nowinandroid/lint/designsystem/DesignSystemIssueRegistry.kt | 1 - 4 files changed, 5 deletions(-) diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/UserPreferencesSerializer.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/UserPreferencesSerializer.kt index 6d1a4ab8b..40c1e210f 100644 --- a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/UserPreferencesSerializer.kt +++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/UserPreferencesSerializer.kt @@ -32,7 +32,6 @@ class UserPreferencesSerializer @Inject constructor() : Serializer> { diff --git a/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemIssueRegistry.kt b/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemIssueRegistry.kt index d951151bb..bb7e971e3 100644 --- a/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemIssueRegistry.kt +++ b/lint/src/main/java/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemIssueRegistry.kt @@ -24,7 +24,6 @@ import com.android.tools.lint.detector.api.CURRENT_API * An issue registry that checks for incorrect usages of Compose Material APIs over equivalents in * the Now in Android design system module. */ -@Suppress("UnstableApiUsage") class DesignSystemIssueRegistry : IssueRegistry() { override val issues = listOf(DesignSystemDetector.ISSUE) From 83fa8c887f0381f87bfe03afa4ee640f64449392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Moczkowski?= Date: Tue, 30 May 2023 09:42:31 +0200 Subject: [PATCH 19/29] Spotless apply Change-Id: I80457acc6a49fe9755240a3d52ff96a8fdfd6b92 --- .../samples/apps/nowinandroid/feature/settings/SettingsDialog.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt index 98e63af56..d8411113d 100644 --- a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt +++ b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt @@ -40,7 +40,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext From 14ce53562f01c3d48274194bf3ec7cd0199a2731 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Tue, 30 May 2023 10:58:30 +0200 Subject: [PATCH 20/29] Update to latest stable AGP 8.0.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0be000440..006b453a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanist = "0.28.0" androidDesugarJdkLibs = "1.2.2" -androidGradlePlugin = "8.0.1" +androidGradlePlugin = "8.0.2" androidxActivity = "1.7.0" androidxAppCompat = "1.5.1" androidxBrowser = "1.4.0" From fecab96e4ce13d08d37c13a5582a5fd78f1465ff Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Tue, 30 May 2023 23:06:40 +0200 Subject: [PATCH 21/29] Keep track of matching `Network`s inside `NetworkCallback` This will ensure the connectivity state remains synchronized with the `ConnectivityManager`. Fixes #714 --- .../util/ConnectivityManagerNetworkMonitor.kt | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt index b0bf9d820..d55520646 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt @@ -20,7 +20,8 @@ import android.content.Context import android.net.ConnectivityManager import android.net.ConnectivityManager.NetworkCallback import android.net.Network -import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkRequest import android.net.NetworkRequest.Builder import android.os.Build.VERSION import android.os.Build.VERSION_CODES @@ -44,36 +45,31 @@ 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() - 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(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) @@ -86,7 +82,8 @@ class ConnectivityManagerNetworkMonitor @Inject constructor( VERSION.SDK_INT >= VERSION_CODES.M -> activeNetwork ?.let(::getNetworkCapabilities) - ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + ?.hasCapability(NET_CAPABILITY_INTERNET) + else -> activeNetworkInfo?.isConnected } ?: false } From 479a2f00b0979cb26a33ec4629048314abccadda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Moczkowski?= Date: Tue, 30 May 2023 12:21:16 +0200 Subject: [PATCH 22/29] Refactor icons --- .../samples/apps/niacatalog/ui/Catalog.kt | 48 +++++++--------- .../navigation/TopLevelDestination.kt | 20 +++---- .../samples/apps/nowinandroid/ui/NiaApp.kt | 55 +++++++------------ .../core/designsystem/icon/NiaIcons.kt | 36 ++++-------- .../src/main/res/drawable/ic_bookmark.xml | 25 --------- .../main/res/drawable/ic_bookmark_border.xml | 25 --------- .../src/main/res/drawable/ic_bookmarks.xml | 28 ---------- .../main/res/drawable/ic_bookmarks_border.xml | 29 ---------- .../src/main/res/drawable/ic_menu_book.xml | 35 ------------ .../main/res/drawable/ic_menu_book_border.xml | 35 ------------ .../src/main/res/drawable/ic_upcoming.xml | 34 ------------ .../main/res/drawable/ic_upcoming_border.xml | 35 ------------ .../nowinandroid/core/ui/NewsResourceCard.kt | 4 +- 13 files changed, 62 insertions(+), 347 deletions(-) delete mode 100644 core/designsystem/src/main/res/drawable/ic_bookmark.xml delete mode 100644 core/designsystem/src/main/res/drawable/ic_bookmark_border.xml delete mode 100644 core/designsystem/src/main/res/drawable/ic_bookmarks.xml delete mode 100644 core/designsystem/src/main/res/drawable/ic_bookmarks_border.xml delete mode 100644 core/designsystem/src/main/res/drawable/ic_menu_book.xml delete mode 100644 core/designsystem/src/main/res/drawable/ic_menu_book_border.xml delete mode 100644 core/designsystem/src/main/res/drawable/ic_upcoming.xml delete mode 100644 core/designsystem/src/main/res/drawable/ic_upcoming_border.xml diff --git a/app-nia-catalog/src/main/java/com/google/samples/apps/niacatalog/ui/Catalog.kt b/app-nia-catalog/src/main/java/com/google/samples/apps/niacatalog/ui/Catalog.kt index 54e4264fa..2624262ad 100644 --- a/app-nia-catalog/src/main/java/com/google/samples/apps/niacatalog/ui/Catalog.kt +++ b/app-nia-catalog/src/main/java/com/google/samples/apps/niacatalog/ui/Catalog.kt @@ -36,7 +36,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip @@ -206,13 +205,13 @@ fun NiaCatalog() { onCheckedChange = { checked -> firstChecked = checked }, icon = { Icon( - painter = painterResource(id = NiaIcons.BookmarkBorder), + imageVector = NiaIcons.BookmarkBorder, contentDescription = null, ) }, checkedIcon = { Icon( - painter = painterResource(id = NiaIcons.Bookmark), + imageVector = NiaIcons.Bookmark, contentDescription = null, ) }, @@ -223,13 +222,13 @@ fun NiaCatalog() { onCheckedChange = { checked -> secondChecked = checked }, icon = { Icon( - painter = painterResource(id = NiaIcons.BookmarkBorder), + imageVector = NiaIcons.BookmarkBorder, contentDescription = null, ) }, checkedIcon = { Icon( - painter = painterResource(id = NiaIcons.Bookmark), + imageVector = NiaIcons.Bookmark, contentDescription = null, ) }, @@ -239,13 +238,13 @@ fun NiaCatalog() { onCheckedChange = {}, icon = { Icon( - painter = painterResource(id = NiaIcons.BookmarkBorder), + imageVector = NiaIcons.BookmarkBorder, contentDescription = null, ) }, checkedIcon = { Icon( - painter = painterResource(id = NiaIcons.Bookmark), + imageVector = NiaIcons.Bookmark, contentDescription = null, ) }, @@ -256,13 +255,13 @@ fun NiaCatalog() { onCheckedChange = {}, icon = { Icon( - painter = painterResource(id = NiaIcons.BookmarkBorder), + imageVector = NiaIcons.BookmarkBorder, contentDescription = null, ) }, checkedIcon = { Icon( - painter = painterResource(id = NiaIcons.Bookmark), + imageVector = NiaIcons.Bookmark, contentDescription = null, ) }, @@ -334,40 +333,31 @@ fun NiaCatalog() { item { Text("Navigation", Modifier.padding(top = 16.dp)) } item { var selectedItem by remember { mutableStateOf(0) } - val items = listOf("For you", "Episodes", "Saved", "Interests") + val items = listOf("For you", "Saved", "Interests") val icons = listOf( NiaIcons.UpcomingBorder, - NiaIcons.MenuBookBorder, NiaIcons.BookmarksBorder, + NiaIcons.Grid3x3, ) val selectedIcons = listOf( NiaIcons.Upcoming, - NiaIcons.MenuBook, NiaIcons.Bookmarks, + NiaIcons.Grid3x3, ) - val tagIcon = NiaIcons.Tag NiaNavigationBar { items.forEachIndexed { index, item -> NiaNavigationBarItem( icon = { - if (index == 3) { - Icon(imageVector = tagIcon, contentDescription = null) - } else { - Icon( - painter = painterResource(id = icons[index]), - contentDescription = item, - ) - } + Icon( + imageVector = icons[index], + contentDescription = item, + ) }, selectedIcon = { - if (index == 3) { - Icon(imageVector = tagIcon, contentDescription = null) - } else { - Icon( - painter = painterResource(id = selectedIcons[index]), - contentDescription = item, - ) - } + Icon( + imageVector = selectedIcons[index], + contentDescription = item, + ) }, label = { Text(item) }, selected = selectedItem == index, diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt index 396ab8b7b..8dbd0fcb6 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt @@ -16,10 +16,8 @@ package com.google.samples.apps.nowinandroid.navigation +import androidx.compose.ui.graphics.vector.ImageVector import com.google.samples.apps.nowinandroid.R -import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon -import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon -import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR @@ -31,26 +29,26 @@ import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR * next within a single destination will be handled directly in composables. */ enum class TopLevelDestination( - val selectedIcon: Icon, - val unselectedIcon: Icon, + val selectedIcon: ImageVector, + val unselectedIcon: ImageVector, val iconTextId: Int, val titleTextId: Int, ) { FOR_YOU( - selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming), - unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder), + selectedIcon = NiaIcons.Upcoming, + unselectedIcon = NiaIcons.UpcomingBorder, iconTextId = forYouR.string.for_you, titleTextId = R.string.app_name, ), BOOKMARKS( - selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks), - unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder), + selectedIcon = NiaIcons.Bookmarks, + unselectedIcon = NiaIcons.BookmarksBorder, iconTextId = bookmarksR.string.saved, titleTextId = bookmarksR.string.saved, ), INTERESTS( - selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3), - unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3), + selectedIcon = NiaIcons.Grid3x3, + unselectedIcon = NiaIcons.Grid3x3, iconTextId = interestsR.string.interests, titleTextId = interestsR.string.interests, ), diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index de321db52..01726c909 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -52,7 +52,6 @@ import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId @@ -70,8 +69,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavig import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRail import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRailItem import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar -import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon -import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors @@ -225,22 +222,16 @@ private fun NiaNavRail( selected = selected, onClick = { onNavigateToDestination(destination) }, icon = { - val icon = if (selected) { - destination.selectedIcon - } else { - destination.unselectedIcon - } - when (icon) { - is ImageVectorIcon -> Icon( - imageVector = icon.imageVector, - contentDescription = null, - ) - - is DrawableResourceIcon -> Icon( - painter = painterResource(id = icon.id), - contentDescription = null, - ) - } + Icon( + imageVector = destination.unselectedIcon, + contentDescription = null, + ) + }, + selectedIcon = { + Icon( + imageVector = destination.selectedIcon, + contentDescription = null, + ) }, label = { Text(stringResource(destination.iconTextId)) }, modifier = if (hasUnread) Modifier.notificationDot() else Modifier, @@ -267,22 +258,16 @@ private fun NiaBottomBar( selected = selected, onClick = { onNavigateToDestination(destination) }, icon = { - val icon = if (selected) { - destination.selectedIcon - } else { - destination.unselectedIcon - } - when (icon) { - is ImageVectorIcon -> Icon( - imageVector = icon.imageVector, - contentDescription = null, - ) - - is DrawableResourceIcon -> Icon( - painter = painterResource(id = icon.id), - contentDescription = null, - ) - } + Icon( + imageVector = destination.unselectedIcon, + contentDescription = null, + ) + }, + selectedIcon = { + Icon( + imageVector = destination.selectedIcon, + contentDescription = null, + ) }, label = { Text(stringResource(destination.iconTextId)) }, modifier = if (hasUnread) Modifier.notificationDot() else Modifier, diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt index ac31fbcdb..8db20689f 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/icon/NiaIcons.kt @@ -16,11 +16,15 @@ package com.google.samples.apps.nowinandroid.core.designsystem.icon -import androidx.annotation.DrawableRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.Bookmarks +import androidx.compose.material.icons.outlined.Upcoming import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Bookmark +import androidx.compose.material.icons.rounded.BookmarkBorder +import androidx.compose.material.icons.rounded.Bookmarks import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Grid3x3 @@ -28,12 +32,9 @@ import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.ShortText -import androidx.compose.material.icons.rounded.Tag +import androidx.compose.material.icons.rounded.Upcoming import androidx.compose.material.icons.rounded.ViewDay -import androidx.compose.material.icons.rounded.VolumeOff -import androidx.compose.material.icons.rounded.VolumeUp import androidx.compose.ui.graphics.vector.ImageVector -import com.google.samples.apps.nowinandroid.core.designsystem.R /** * Now in Android icons. Material icons are [ImageVector]s, custom icons are drawable resource IDs. @@ -41,32 +42,19 @@ import com.google.samples.apps.nowinandroid.core.designsystem.R object NiaIcons { val Add = Icons.Rounded.Add val ArrowBack = Icons.Rounded.ArrowBack - val Bookmark = R.drawable.ic_bookmark - val BookmarkBorder = R.drawable.ic_bookmark_border - val Bookmarks = R.drawable.ic_bookmarks - val BookmarksBorder = R.drawable.ic_bookmarks_border + val Bookmark = Icons.Rounded.Bookmark + val BookmarkBorder = Icons.Rounded.BookmarkBorder + val Bookmarks = Icons.Rounded.Bookmarks + val BookmarksBorder = Icons.Outlined.Bookmarks val Check = Icons.Rounded.Check val Close = Icons.Rounded.Close val Grid3x3 = Icons.Rounded.Grid3x3 - val MenuBook = R.drawable.ic_menu_book - val MenuBookBorder = R.drawable.ic_menu_book_border val MoreVert = Icons.Default.MoreVert val Person = Icons.Rounded.Person val Search = Icons.Rounded.Search val Settings = Icons.Rounded.Settings val ShortText = Icons.Rounded.ShortText - val Tag = Icons.Rounded.Tag - val Upcoming = R.drawable.ic_upcoming - val UpcomingBorder = R.drawable.ic_upcoming_border + val Upcoming = Icons.Rounded.Upcoming + val UpcomingBorder = Icons.Outlined.Upcoming val ViewDay = Icons.Rounded.ViewDay - val VolumeOff = Icons.Rounded.VolumeOff - val VolumeUp = Icons.Rounded.VolumeUp -} - -/** - * A sealed class to make dealing with [ImageVector] and [DrawableRes] icons easier. - */ -sealed class Icon { - data class ImageVectorIcon(val imageVector: ImageVector) : Icon() - data class DrawableResourceIcon(@DrawableRes val id: Int) : Icon() } diff --git a/core/designsystem/src/main/res/drawable/ic_bookmark.xml b/core/designsystem/src/main/res/drawable/ic_bookmark.xml deleted file mode 100644 index 29b7e40a7..000000000 --- a/core/designsystem/src/main/res/drawable/ic_bookmark.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/core/designsystem/src/main/res/drawable/ic_bookmark_border.xml b/core/designsystem/src/main/res/drawable/ic_bookmark_border.xml deleted file mode 100644 index 1d4b4aca9..000000000 --- a/core/designsystem/src/main/res/drawable/ic_bookmark_border.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/core/designsystem/src/main/res/drawable/ic_bookmarks.xml b/core/designsystem/src/main/res/drawable/ic_bookmarks.xml deleted file mode 100644 index ed6e84f81..000000000 --- a/core/designsystem/src/main/res/drawable/ic_bookmarks.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - diff --git a/core/designsystem/src/main/res/drawable/ic_bookmarks_border.xml b/core/designsystem/src/main/res/drawable/ic_bookmarks_border.xml deleted file mode 100644 index 64f0b5159..000000000 --- a/core/designsystem/src/main/res/drawable/ic_bookmarks_border.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - diff --git a/core/designsystem/src/main/res/drawable/ic_menu_book.xml b/core/designsystem/src/main/res/drawable/ic_menu_book.xml deleted file mode 100644 index e81276888..000000000 --- a/core/designsystem/src/main/res/drawable/ic_menu_book.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - diff --git a/core/designsystem/src/main/res/drawable/ic_menu_book_border.xml b/core/designsystem/src/main/res/drawable/ic_menu_book_border.xml deleted file mode 100644 index 04ec651f6..000000000 --- a/core/designsystem/src/main/res/drawable/ic_menu_book_border.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - diff --git a/core/designsystem/src/main/res/drawable/ic_upcoming.xml b/core/designsystem/src/main/res/drawable/ic_upcoming.xml deleted file mode 100644 index a05017e74..000000000 --- a/core/designsystem/src/main/res/drawable/ic_upcoming.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - diff --git a/core/designsystem/src/main/res/drawable/ic_upcoming_border.xml b/core/designsystem/src/main/res/drawable/ic_upcoming_border.xml deleted file mode 100644 index 5f3151232..000000000 --- a/core/designsystem/src/main/res/drawable/ic_upcoming_border.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index 46521a320..009fb1249 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -183,13 +183,13 @@ fun BookmarkButton( modifier = modifier, icon = { Icon( - painter = painterResource(NiaIcons.BookmarkBorder), + imageVector = NiaIcons.BookmarkBorder, contentDescription = stringResource(R.string.bookmark), ) }, checkedIcon = { Icon( - painter = painterResource(NiaIcons.Bookmark), + imageVector = NiaIcons.Bookmark, contentDescription = stringResource(R.string.unbookmark), ) }, From 7b30720b2526903b74b5fd0a689d5810409f60be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Moczkowski?= Date: Wed, 31 May 2023 17:38:08 +0200 Subject: [PATCH 23/29] Remove nested scaffold from the bookmarks screen Change-Id: Ie8b6f160d341156a6f9c02c0ca7f530095fb2950 --- .../nowinandroid/navigation/NiaNavHost.kt | 6 ++- .../samples/apps/nowinandroid/ui/NiaApp.kt | 10 +++- .../feature/bookmarks/BookmarksScreenTest.kt | 4 ++ .../feature/bookmarks/BookmarksScreen.kt | 51 ++++++++----------- .../navigation/BookmarksNavigation.kt | 7 ++- 5 files changed, 44 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt index e43dfaba7..1d600b53d 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt @@ -39,6 +39,7 @@ import com.google.samples.apps.nowinandroid.ui.NiaAppState @Composable fun NiaNavHost( appState: NiaAppState, + onShowSnackbar: suspend (String, String?) -> Boolean, modifier: Modifier = Modifier, startDestination: String = forYouNavigationRoute, ) { @@ -50,7 +51,10 @@ fun NiaNavHost( ) { // TODO: handle topic clicks from each top level destination forYouScreen(onTopicClick = {}) - bookmarksScreen(onTopicClick = {}) + bookmarksScreen( + onTopicClick = navController::navigateToTopic, + onShowSnackbar = onShowSnackbar, + ) searchScreen( onBackClick = navController::popBackStack, onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) }, diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 01726c909..aa85afebd 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -33,8 +33,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration.Indefinite +import androidx.compose.material3.SnackbarDuration.Short import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.windowsizeclass.WindowSizeClass @@ -195,7 +197,13 @@ fun NiaApp( ) } - NiaNavHost(appState) + NiaNavHost(appState = appState, onShowSnackbar = { message, action -> + snackbarHostState.showSnackbar( + message = message, + actionLabel = action, + duration = Short, + ) == ActionPerformed + }) } // TODO: We may want to add padding or spacer when the snackbar is shown so that diff --git a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt index 680c6dcf7..6e432f2ab 100644 --- a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt +++ b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt @@ -50,6 +50,7 @@ class BookmarksScreenTest { composeTestRule.setContent { BookmarksScreen( feedState = NewsFeedUiState.Loading, + onShowSnackbar = { _, _ -> false }, removeFromBookmarks = {}, onTopicClick = {}, onNewsResourceViewed = {}, @@ -70,6 +71,7 @@ class BookmarksScreenTest { feedState = NewsFeedUiState.Success( userNewsResourcesTestData.take(2), ), + onShowSnackbar = { _, _ -> false }, removeFromBookmarks = {}, onTopicClick = {}, onNewsResourceViewed = {}, @@ -110,6 +112,7 @@ class BookmarksScreenTest { feedState = NewsFeedUiState.Success( userNewsResourcesTestData.take(2), ), + onShowSnackbar = { _, _ -> false }, removeFromBookmarks = { newsResourceId -> assertEquals(userNewsResourcesTestData[0].id, newsResourceId) removeFromBookmarksCalled = true @@ -144,6 +147,7 @@ class BookmarksScreenTest { composeTestRule.setContent { BookmarksScreen( feedState = NewsFeedUiState.Success(emptyList()), + onShowSnackbar = { _, _ -> false }, removeFromBookmarks = {}, onTopicClick = {}, onNewsResourceViewed = {}, diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index 25412e851..0f15e29b0 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -19,7 +19,6 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks import androidx.annotation.VisibleForTesting import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -35,19 +34,12 @@ import androidx.compose.foundation.lazy.grid.GridCells.Adaptive import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration.Short -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -79,12 +71,14 @@ import com.google.samples.apps.nowinandroid.core.ui.newsFeed @Composable internal fun BookmarksRoute( onTopicClick: (String) -> Unit, + onShowSnackbar: suspend (String, String?) -> Boolean, modifier: Modifier = Modifier, viewModel: BookmarksViewModel = hiltViewModel(), ) { val feedState by viewModel.feedUiState.collectAsStateWithLifecycle() BookmarksScreen( feedState = feedState, + onShowSnackbar = onShowSnackbar, removeFromBookmarks = viewModel::removeFromSavedResources, onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) }, onTopicClick = onTopicClick, @@ -98,11 +92,11 @@ internal fun BookmarksRoute( /** * Displays the user's bookmarked articles. Includes support for loading and empty states. */ -@OptIn(ExperimentalMaterial3Api::class) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @Composable internal fun BookmarksScreen( feedState: NewsFeedUiState, + onShowSnackbar: suspend (String, String?) -> Boolean, removeFromBookmarks: (String) -> Unit, onNewsResourceViewed: (String) -> Unit, onTopicClick: (String) -> Unit, @@ -113,18 +107,14 @@ internal fun BookmarksScreen( ) { val bookmarkRemovedMessage = stringResource(id = R.string.bookmark_removed) val undoText = stringResource(id = R.string.undo) - val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(shouldDisplayUndoBookmark) { if (shouldDisplayUndoBookmark) { - val snackBarResult = snackbarHostState.showSnackbar( - message = bookmarkRemovedMessage, - actionLabel = undoText, - duration = Short, - ) - when (snackBarResult) { - ActionPerformed -> { undoBookmarkRemoval() } - else -> { clearUndoState() } + val snackBarResult = onShowSnackbar(bookmarkRemovedMessage, undoText) + if (snackBarResult) { + undoBookmarkRemoval() + } else { + clearUndoState() } } } @@ -140,20 +130,21 @@ internal fun BookmarksScreen( onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } - Scaffold(snackbarHost = { SnackbarHost(hostState = snackbarHostState) }) { - Box( - modifier = Modifier.padding(it).fillMaxSize(), - ) { - when (feedState) { - Loading -> LoadingState(modifier) - is Success -> if (feedState.feed.isNotEmpty()) { - BookmarksGrid(feedState, removeFromBookmarks, onNewsResourceViewed, onTopicClick, modifier) - } else { - EmptyState(modifier) - } - } + when (feedState) { + Loading -> LoadingState(modifier) + is Success -> if (feedState.feed.isNotEmpty()) { + BookmarksGrid( + feedState, + removeFromBookmarks, + onNewsResourceViewed, + onTopicClick, + modifier, + ) + } else { + EmptyState(modifier) } } + TrackScreenViewEvent(screenName = "Saved") } diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt index eeb7f1576..ebcde4ab1 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt @@ -28,8 +28,11 @@ fun NavController.navigateToBookmarks(navOptions: NavOptions? = null) { this.navigate(bookmarksRoute, navOptions) } -fun NavGraphBuilder.bookmarksScreen(onTopicClick: (String) -> Unit) { +fun NavGraphBuilder.bookmarksScreen( + onTopicClick: (String) -> Unit, + onShowSnackbar: suspend (String, String?) -> Boolean, +) { composable(route = bookmarksRoute) { - BookmarksRoute(onTopicClick) + BookmarksRoute(onTopicClick, onShowSnackbar) } } From 12abe939d7927ec04d074d7993e0b667b53c5fb7 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Fri, 2 Jun 2023 22:29:28 +0200 Subject: [PATCH 24/29] Add IntelliJ IDEA icons --- .idea/icon.png | Bin 0 -> 9710 bytes .idea/icon_dark.png | Bin 0 -> 10926 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .idea/icon.png create mode 100644 .idea/icon_dark.png diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c91b32cd910818b03875c28181e1a79c51ffc9de GIT binary patch literal 9710 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 literal 0 HcmV?d00001 diff --git a/.idea/icon_dark.png b/.idea/icon_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..cc8a9c1b015bec59cb4e4070ff648857844d67e1 GIT binary patch literal 10926 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 Date: Sat, 3 Jun 2023 14:09:55 +0200 Subject: [PATCH 25/29] Apply changes from review comments --- .../core/data/util/ConnectivityManagerNetworkMonitor.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt index d55520646..c88125be8 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt @@ -20,7 +20,7 @@ import android.content.Context import android.net.ConnectivityManager import android.net.ConnectivityManager.NetworkCallback import android.net.Network -import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.NetworkRequest.Builder import android.os.Build.VERSION @@ -63,7 +63,9 @@ class ConnectivityManagerNetworkMonitor @Inject constructor( } } - val request = Builder().addCapability(NET_CAPABILITY_INTERNET).build() + val request = Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() connectivityManager.registerNetworkCallback(request, callback) /** @@ -82,7 +84,7 @@ class ConnectivityManagerNetworkMonitor @Inject constructor( VERSION.SDK_INT >= VERSION_CODES.M -> activeNetwork ?.let(::getNetworkCapabilities) - ?.hasCapability(NET_CAPABILITY_INTERNET) + ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) else -> activeNetworkInfo?.isConnected } ?: false From 6f067049947cb83cd9652970d7002448ba6f5eae Mon Sep 17 00:00:00 2001 From: Alejandra Stamato Date: Mon, 5 Jun 2023 13:52:44 +0100 Subject: [PATCH 26/29] Added scroll test for topic list --- .../interests/ScrollTopicListBenchmark.kt | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListBenchmark.kt diff --git a/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListBenchmark.kt b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListBenchmark.kt new file mode 100644 index 000000000..b43d3a84b --- /dev/null +++ b/benchmarks/src/main/java/com/google/samples/apps/nowinandroid/interests/ScrollTopicListBenchmark.kt @@ -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() + } + } +} From dd70bbd589f48306004846e31ec69021d4228b0b Mon Sep 17 00:00:00 2001 From: Amaury Medeiros Date: Wed, 7 Jun 2023 12:00:47 +0100 Subject: [PATCH 27/29] Fix ForYouScreen Compose Previews Permissions should only be called in an Activity context, which is a layoutlib limitation. We need to avoid launching the permission request when in LocalInspectionMode, otherwise we'll have render errors on the ForYouScreen previews. --- .../samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index ebc0a6fe9..012b98608 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -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 @@ -406,6 +407,8 @@ fun TopicIcon( @Composable @OptIn(ExperimentalPermissionsApi::class) private fun NotificationPermissionEffect() { + // Permissions should be called from in 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, From 87c27f6b82e23ca86f51b677ddb194b1e1e48642 Mon Sep 17 00:00:00 2001 From: Amaury Medeiros Date: Wed, 7 Jun 2023 14:33:15 +0100 Subject: [PATCH 28/29] Apply suggested changes to inline comment --- .../samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 012b98608..f71be33e9 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -407,7 +407,8 @@ fun TopicIcon( @Composable @OptIn(ExperimentalPermissionsApi::class) private fun NotificationPermissionEffect() { - // Permissions should be called from in an Activity Context, which is not present in previews + // Permissions should be called from in an Activity Context, which is not present + // in previews if (LocalInspectionMode.current) return if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) return val notificationsPermissionState = rememberPermissionState( From 512930c2396cb5298a37d1989c9b7e2faa822529 Mon Sep 17 00:00:00 2001 From: Amaury Medeiros Date: Wed, 7 Jun 2023 14:37:05 +0100 Subject: [PATCH 29/29] Fix inline comment as suggested in PR --- .../samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index f71be33e9..70cc7e541 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -407,7 +407,7 @@ fun TopicIcon( @Composable @OptIn(ExperimentalPermissionsApi::class) private fun NotificationPermissionEffect() { - // Permissions should be called from in an Activity Context, which is not present + // 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