fix(onboarding): request notification permission after welcome screen renders #168
@@ -227,6 +227,7 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(libs.androidx.espresso.intents)
|
||||
androidTestImplementation(libs.androidx.test.rules)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.androidx.room.testing)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.onboarding
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -18,6 +20,7 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -54,6 +57,19 @@ class OnboardingFlowTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
// OnboardingWelcomeScreen requests POST_NOTIFICATIONS when it first composes (#151). On API 33+
|
||||
// that runtime dialog would pop over the test, backgrounding the activity and leaving the compose
|
||||
// rule with "No compose hierarchies found". Pre-grant it so the flow runs uninterrupted; the
|
||||
// permission only exists on API 33+, so below TIRAMISU grant nothing (granting a nonexistent
|
||||
// permission errors on older devices).
|
||||
@get:Rule
|
||||
val notificationPermission: GrantPermissionRule =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
GrantPermissionRule.grant(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
GrantPermissionRule.grant()
|
||||
}
|
||||
|
||||
private fun string(resId: Int, vararg args: Any) = composeTestRule.activity.getString(resId, *args)
|
||||
|
||||
// Generous cap for the slow, animation-disabled CI matrix emulators; waitUntil returns as soon
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -84,7 +75,6 @@ class MainActivity : FragmentActivity() {
|
||||
setContent {
|
||||
val dynamicColor by settingsRepository.dynamicColor.collectAsStateWithLifecycle(initialValue = true)
|
||||
LibreMailTheme(dynamicColor = dynamicColor) {
|
||||
NotificationPermissionEffect()
|
||||
// Gate the whole app behind the screen-lock when app-lock is enabled. When it is off
|
||||
// the gate resolves straight to the content, so this is a no-op for most users.
|
||||
AppLockGateHost {
|
||||
@@ -106,18 +96,3 @@ class MainActivity : FragmentActivity() {
|
||||
NotificationIntents.messageId(intent)?.let { pendingOpenMessageId.value = it }
|
||||
}
|
||||
}
|
||||
|
||||
/** Requests POST_NOTIFICATIONS once on first launch (no-op if already granted). */
|
||||
@Composable
|
||||
private fun NotificationPermissionEffect() {
|
||||
val context = LocalContext.current
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {}
|
||||
LaunchedEffect(Unit) {
|
||||
// POST_NOTIFICATIONS is a runtime permission only on Android 13 (API 33)+. On older
|
||||
// versions notifications are enabled by default, so there's nothing to request.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return@LaunchedEffect
|
||||
val granted = ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (!granted) launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.ui.onboarding
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -18,11 +23,14 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.libremail.R
|
||||
|
||||
/**
|
||||
@@ -31,6 +39,11 @@ import org.libremail.R
|
||||
*/
|
||||
@Composable
|
||||
fun OnboardingWelcomeScreen(onAddAccount: () -> Unit) {
|
||||
// Requested from here, rather than the Activity root, so the system permission dialog appears
|
||||
// once this screen (with onboarding context behind it) is actually visible instead of racing
|
||||
// the cold-start/splash transition (#151). Already-onboarded users skip onboarding entirely, so
|
||||
// this composable — and the request — never runs for them.
|
||||
NotificationPermissionEffect()
|
||||
Scaffold { padding ->
|
||||
WelcomeContent(
|
||||
onAddAccount = onAddAccount,
|
||||
@@ -83,3 +96,18 @@ fun WelcomeContent(onAddAccount: () -> Unit, modifier: Modifier = Modifier) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requests POST_NOTIFICATIONS once on first launch (no-op if already granted). */
|
||||
@Composable
|
||||
private fun NotificationPermissionEffect() {
|
||||
val context = LocalContext.current
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {}
|
||||
LaunchedEffect(Unit) {
|
||||
// POST_NOTIFICATIONS is a runtime permission only on Android 13 (API 33)+. On older
|
||||
// versions notifications are enabled by default, so there's nothing to request.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return@LaunchedEffect
|
||||
val granted = ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (!granted) launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,14 +62,16 @@ Nothing else. Notably **absent** (worth stating in any review exchange):
|
||||
`notifications/MailNotifier.kt` (no push/cloud-messaging service; lock-screen content
|
||||
redacted via `VISIBILITY_PRIVATE`); (2) the persistent low-importance status notification
|
||||
Android requires while the IMAP IDLE foreground service runs (`push/IdleService.kt:120`).
|
||||
- **Request flow:** once at first launch, API 33+ only (`MainActivity.kt`
|
||||
`NotificationPermissionEffect`). If denied, `MailNotifier.notifyNewMail` no-ops (permission
|
||||
- **Request flow:** once, when the onboarding welcome screen appears, API 33+ only
|
||||
(`ui/onboarding/OnboardingWelcomeScreen.kt` `NotificationPermissionEffect`, scoped to that
|
||||
screen's composition so the system dialog shows onboarding context instead of racing the
|
||||
cold-start/splash transition — #151). If denied, `MailNotifier.notifyNewMail` no-ops (permission
|
||||
re-checked before every post, `MailNotifier.kt:134`); mail sync itself is unaffected.
|
||||
- **Play-Console justification text (if asked):**
|
||||
> Notifies the user of newly received email (per-account channels, generated on the device
|
||||
> from the user's own mailbox — no push service) and shows the persistent status notification
|
||||
> Android requires for the optional foreground IMAP IDLE connection. Requested once at first
|
||||
> launch; all app functions except notifications work if declined.
|
||||
> Android requires for the optional foreground IMAP IDLE connection. Requested once, when the
|
||||
> onboarding welcome screen appears; all app functions except notifications work if declined.
|
||||
|
||||
## `FOREGROUND_SERVICE_DATA_SYNC` (requires the Play Console FGS declaration)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ angusMail = "2.0.5"
|
||||
junit = "4.13.2"
|
||||
androidxJunit = "1.2.1"
|
||||
espresso = "3.7.0"
|
||||
androidxTestRules = "1.7.0"
|
||||
turbine = "1.2.1"
|
||||
mockk = "1.14.11"
|
||||
greenmail = "2.1.9"
|
||||
@@ -110,6 +111,7 @@ greenmail = { group = "com.icegreen", name = "greenmail", version.ref = "greenma
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
|
||||
androidx-espresso-intents = { group = "androidx.test.espresso", name = "espresso-intents", version.ref = "espresso" }
|
||||
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user