From aa701064a278aedc4d17775c2a895026a6e6b341 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 22 Apr 2023 15:38:35 +0200 Subject: [PATCH 01/35] Add Application-wide `CoroutineScope` in the DI graph Following the work of #607. --- .../core/network/di/CoroutineScopesModule.kt | 39 +++++++++++++++++++ .../datastore/test/TestDataStoreModule.kt | 7 +--- .../core/datastore/di/DataStoreModule.kt | 6 +-- 3 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/CoroutineScopesModule.kt 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 new file mode 100644 index 000000000..bc44e394d --- /dev/null +++ b/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/CoroutineScopesModule.kt @@ -0,0 +1,39 @@ +/* + * 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.network.di + +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object CoroutineScopesModule { + @Provides + @Singleton + @Dispatcher(IO) + fun providesIOCoroutineScope( + @Dispatcher(IO) ioDispatcher: CoroutineDispatcher, + ): CoroutineScope = CoroutineScope(SupervisorJob() + ioDispatcher) +} 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 fad7ac382..cb7e38db7 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 @@ -27,9 +27,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.components.SingletonComponent import dagger.hilt.testing.TestInstallIn -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import org.junit.rules.TemporaryFolder import javax.inject.Singleton @@ -43,13 +41,12 @@ object TestDataStoreModule { @Provides @Singleton fun providesUserPreferencesDataStore( - @Dispatcher(IO) ioDispatcher: CoroutineDispatcher, + @Dispatcher(IO) ioScope: CoroutineScope, userPreferencesSerializer: UserPreferencesSerializer, tmpFolder: TemporaryFolder, ): DataStore = tmpFolder.testUserPreferencesDataStore( - // TODO: Provide an application-wide CoroutineScope in the DI graph - coroutineScope = CoroutineScope(SupervisorJob() + ioDispatcher), + coroutineScope = ioScope, userPreferencesSerializer = userPreferencesSerializer, ) } 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 895e22307..89a6eb734 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 @@ -30,9 +30,7 @@ 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 kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -43,12 +41,12 @@ object DataStoreModule { @Singleton fun providesUserPreferencesDataStore( @ApplicationContext context: Context, - @Dispatcher(IO) ioDispatcher: CoroutineDispatcher, + @Dispatcher(IO) ioScope: CoroutineScope, userPreferencesSerializer: UserPreferencesSerializer, ): DataStore = DataStoreFactory.create( serializer = userPreferencesSerializer, - scope = CoroutineScope(ioDispatcher + SupervisorJob()), + scope = ioScope, migrations = listOf( IntToStringIdsMigration, ), From 5a43b4b74e66670b95b6e821dfed439ea98562ca Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 22 Apr 2023 15:46:15 +0200 Subject: [PATCH 02/35] Run Spotless --- .../core/network/di/CoroutineScopesModule.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 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 bc44e394d..b73c63568 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 @@ -1,17 +1,17 @@ /* * 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 + * 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 + * 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. + * 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.network.di From 331e8cffaca5f7fae93267c9d25a7231106fb6bf Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Fri, 19 May 2023 11:45:38 +0100 Subject: [PATCH 03/35] Grant `POST_NOTIFICATIONS` permission in instrumented tests --- .../apps/nowinandroid/feature/foryou/ForYouScreenTest.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 eb27473bb..404ad55b9 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,6 +16,7 @@ package com.google.samples.apps.nowinandroid.feature.foryou +import android.Manifest import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.ui.test.assertHasClickAction @@ -28,6 +29,8 @@ 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.testing.data.followableTopicTestData import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState @@ -35,6 +38,10 @@ import org.junit.Rule import org.junit.Test class ForYouScreenTest { + + @get:Rule + val permissionTestRule: GrantPermissionRule = grant(Manifest.permission.POST_NOTIFICATIONS) + @get:Rule val composeTestRule = createAndroidComposeRule() From 69879ff874161253b255de4e5c7bdf5fb024f228 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Fri, 19 May 2023 12:28:01 +0100 Subject: [PATCH 04/35] Grant permission only if TIRAMISU or above --- .../apps/nowinandroid/feature/foryou/ForYouScreenTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 404ad55b9..7a750a13d 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 @@ -17,6 +17,8 @@ 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 @@ -40,7 +42,9 @@ import org.junit.Test class ForYouScreenTest { @get:Rule - val permissionTestRule: GrantPermissionRule = grant(Manifest.permission.POST_NOTIFICATIONS) + val permissionTestRule: GrantPermissionRule = + if (SDK_INT < TIRAMISU) grant() + else grant(Manifest.permission.POST_NOTIFICATIONS) @get:Rule val composeTestRule = createAndroidComposeRule() From a93cce92f712a8d067751f751e7ffce0de544c2f Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Fri, 19 May 2023 12:51:35 +0100 Subject: [PATCH 05/35] Update ForYouScreenTest.kt --- .../apps/nowinandroid/feature/foryou/ForYouScreenTest.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 7a750a13d..b138cba06 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 @@ -43,8 +43,11 @@ class ForYouScreenTest { @get:Rule val permissionTestRule: GrantPermissionRule = - if (SDK_INT < TIRAMISU) grant() - else grant(Manifest.permission.POST_NOTIFICATIONS) + if (SDK_INT >= TIRAMISU) { + grant(Manifest.permission.POST_NOTIFICATIONS) + } else { + grant() + } @get:Rule val composeTestRule = createAndroidComposeRule() From ae50f3d332d5049843bc0458c743dce46fd697fc Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Wed, 24 May 2023 18:46:33 +0100 Subject: [PATCH 06/35] 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 07/35] 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 01e5a68a8d9e9fc65619f2e82e94b66eac92b0b0 Mon Sep 17 00:00:00 2001 From: Vixb Date: Thu, 25 May 2023 08:11:51 +0800 Subject: [PATCH 08/35] Replace NiaTopLevelNavigation with TopLevelDestination based on #186 (#723) * Replace NiaTopLevelNavigation with TopLevelDestination based on #186 * Update ModularizationLearningJourney.md --- docs/ModularizationLearningJourney.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ModularizationLearningJourney.md b/docs/ModularizationLearningJourney.md index 32f1c2249..81e35c436 100644 --- a/docs/ModularizationLearningJourney.md +++ b/docs/ModularizationLearningJourney.md @@ -95,7 +95,7 @@ The Now in Android app contains the following types of modules: * The `app` module - contains app level and scaffolding classes that bind the rest of the codebase, such as `MainActivity`, `NiaApp` and app-level controlled navigation. A good example of this is the navigation setup through `NiaNavHost` and the bottom navigation bar setup - through `NiaTopLevelNavigation`. The `app` module depends on all `feature` modules and + through `TopLevelDestination`. The `app` module depends on all `feature` modules and required `core` modules. * `feature:` modules - feature specific modules which are scoped to handle a single responsibility @@ -132,7 +132,7 @@ Using the above modularization strategy, the Now in Android app has the followin Brings everything together required for the app to function correctly. This includes UI scaffolding and navigation. NiaApp, MainActivity
- App-level controlled navigation via NiaNavHost, NiaTopLevelNavigation + App-level controlled navigation via NiaNavHost, NiaAppState, TopLevelDestination From cd6ad7d575a82c6a88c9c8e0e13841743d3031e7 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Thu, 25 May 2023 08:32:26 +0100 Subject: [PATCH 09/35] 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 10/35] 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 11/35] 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 12/35] 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 13/35] 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 14/35] 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 15/35] 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 16/35] 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 17/35] 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 18/35] 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 19/35] 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 20/35] 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 21/35] 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 22/35] 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 23/35] 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 24/35] 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 25/35] 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 26/35] 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 27/35] 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 28/35] 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 29/35] 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 30/35] 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 31/35] 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 32/35] 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 33/35] 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 34/35] 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 35/35] 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