From 823c4db2013a27616b9d88aaca3349d87c13160d Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Wed, 24 May 2023 20:11:36 +0200 Subject: [PATCH 01/10] 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 8c8c7611ce9461ca0df994d5ec81fcd18b3b2ee0 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 27 May 2023 12:07:36 +0200 Subject: [PATCH 02/10] 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 97a55c9dd52168a6c352e19e4cdbcff1b860a31f Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 27 May 2023 20:51:41 +0200 Subject: [PATCH 03/10] 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 fecab96e4ce13d08d37c13a5582a5fd78f1465ff Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Tue, 30 May 2023 23:06:40 +0200 Subject: [PATCH 04/10] 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 12abe939d7927ec04d074d7993e0b667b53c5fb7 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Fri, 2 Jun 2023 22:29:28 +0200 Subject: [PATCH 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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 10/10] 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