diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 297fe865f..b8446e780 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -165,8 +165,6 @@ the latest Android toolkit:
- [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) for user
preferences storage.
- [Coil](https://github.com/coil-kt/coil) for asynchronous loading of images.
-- [Accompanist](https://github.com/google/accompanist) for the handling of user permissions in
- Jetpack Compose.
- [Vico](https://github.com/patrykandpatrick/vico) for charts.
- [Lottie Compose](https://github.com/airbnb/lottie-android) for animations.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 60f168dc2..786fb164b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -174,9 +174,6 @@ dependencies {
implementation(libs.androidx.datastore.core.android)
implementation(libs.androidx.datastore.preferences)
- // Google accompanist to handle permissions in jetpack compose
- implementation(libs.accompanist.permissions)
-
// Lottie animations for jetpack compose
implementation(libs.lottie.compose)
diff --git a/app/src/main/java/org/librefit/ui/screens/home/HomeScreen.kt b/app/src/main/java/org/librefit/ui/screens/home/HomeScreen.kt
index 407895a5d..f9a17404d 100644
--- a/app/src/main/java/org/librefit/ui/screens/home/HomeScreen.kt
+++ b/app/src/main/java/org/librefit/ui/screens/home/HomeScreen.kt
@@ -9,6 +9,7 @@
package org.librefit.ui.screens.home
import android.Manifest
+import android.content.pm.PackageManager
import android.os.Build
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
@@ -38,16 +39,19 @@ import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
@@ -56,13 +60,14 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
-import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.isGranted
-import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.collections.immutable.persistentListOf
import org.librefit.R
import org.librefit.enums.InfoMode
@@ -81,27 +86,49 @@ import org.librefit.ui.theme.LibreFitTheme
import org.librefit.util.Formatter
import kotlin.random.Random
-@OptIn(ExperimentalPermissionsApi::class, ExperimentalSharedTransitionApi::class)
+@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedTransitionScope.HomeScreen(
navController: NavHostController,
animatedVisibilityScope: AnimatedVisibilityScope,
viewModel: HomeScreenViewModel = hiltViewModel(),
) {
+ val context = LocalContext.current
+ var hasNotificationPermission by remember {
+ mutableStateOf(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ } else {
+ true
+ }
+ )
+ }
+
+ val lifecycleOwner = LocalLifecycleOwner.current
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ hasNotificationPermission = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
val showKeepAndroidOpen by viewModel.showKeepAndroidOpen.collectAsStateWithLifecycle()
val requestPermissionNextTime by viewModel.requestPermissionNextTime.collectAsStateWithLifecycle()
- val notificationPermissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- rememberPermissionState(
- Manifest.permission.POST_NOTIFICATIONS
- )
- } else {
- //Permission granted by default below Tiramisu
- null
- }
-
val routines by viewModel.routines.collectAsStateWithLifecycle()
val runningWorkout by viewModel.runningWorkout.collectAsStateWithLifecycle()
@@ -115,8 +142,6 @@ fun SharedTransitionScope.HomeScreen(
showKeepAndroidOpen = showKeepAndroidOpen,
onKeepAndroidOpenCheckboxChange = viewModel::saveKeepOpenAndroidCheckbox,
navigateToRoutine = { workoutId ->
- val hasNotificationPermission = notificationPermissionState?.status?.isGranted != false
-
val requestPermission = !hasNotificationPermission && requestPermissionNextTime
if (requestPermission) {
@@ -470,4 +495,4 @@ fun HomeScreenPreview() {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/librefit/ui/screens/shared/RequestPermissionScreen.kt b/app/src/main/java/org/librefit/ui/screens/shared/RequestPermissionScreen.kt
index b12a54659..b0904ded3 100644
--- a/app/src/main/java/org/librefit/ui/screens/shared/RequestPermissionScreen.kt
+++ b/app/src/main/java/org/librefit/ui/screens/shared/RequestPermissionScreen.kt
@@ -9,7 +9,13 @@
package org.librefit.ui.screens.shared
import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
import android.os.Build
+import android.provider.Settings
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -27,30 +33,35 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
-import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.isGranted
-import com.google.accompanist.permissions.rememberPermissionState
import org.librefit.R
import org.librefit.enums.userPreferences.ThemeMode
import org.librefit.nav.Route
import org.librefit.ui.components.LibreFitLazyColumn
import org.librefit.ui.components.LibreFitScaffold
import org.librefit.ui.components.animations.PreferencesLottie
+import org.librefit.ui.components.dialogs.ConfirmDialog
import org.librefit.ui.theme.LibreFitTheme
import kotlin.random.Random
-
-@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun RequestPermissionScreen(
navController: NavHostController,
@@ -58,20 +69,108 @@ fun RequestPermissionScreen(
requestPermissionNextTime: Boolean,
saveRequestPermissionAgainPreference: (Boolean) -> Unit
) {
- val notificationPermissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- rememberPermissionState(
- Manifest.permission.POST_NOTIFICATIONS
+ val context = LocalContext.current
+
+ var hasNotificationPermission by remember {
+ mutableStateOf(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ } else {
+ true
+ }
+ )
+ }
+
+ var showSettingsDialog by remember { mutableStateOf(false) }
+
+ if (showSettingsDialog) {
+ ConfirmDialog(
+ title = stringResource(R.string.notifications_permission),
+ text = stringResource(R.string.notifications_permission_permanently_denied_desc),
+ confirmText = stringResource(R.string.open_settings),
+ onConfirm = {
+ showSettingsDialog = false
+ val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
+ // This is the standard way to target app settings
+ putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
+ }
+ context.startActivity(intent)
+ },
+ onDismiss = {
+ showSettingsDialog = false
+ }
)
- } else {
- //Permission granted by default below Tiramisu
- null
+ }
+
+ val launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ hasNotificationPermission = isGranted
+ if (!isGranted) {
+ val activity = context as? Activity
+ if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val showRationale = ActivityCompat.shouldShowRequestPermissionRationale(
+ activity,
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+ if (!showRationale) {
+ // If it's denied and shouldShowRequestPermissionRationale is false,
+ // it means the user checked "Don't ask again" or it is permanently blocked.
+ showSettingsDialog = true
+ }
+ }
+ }
+ }
+
+ val lifecycleOwner = LocalLifecycleOwner.current
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ hasNotificationPermission = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ val handleNotificationPermissionRequest = {
+ val activity = context as? Activity
+ if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val showRationale = ActivityCompat.shouldShowRequestPermissionRationale(
+ activity,
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+ val checkPermission = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+
+ if (checkPermission == PackageManager.PERMISSION_DENIED && !showRationale) {
+ // We are in a permanently denied state (or pre-request state)
+ // To be safe, we attempt to launch the launcher. If the launcher returns
+ // immediately with false, we then trigger the Settings Redirect Dialog.
+ launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ } else {
+ launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
}
RequestPermissionsScreenContent(
navController = navController,
requestPermissionNextTime = requestPermissionNextTime,
- hasNotificationPermission = notificationPermissionState?.status?.isGranted != false,
- launchNotificationPermissionRequest = { notificationPermissionState?.launchPermissionRequest() },
+ hasNotificationPermission = hasNotificationPermission,
+ handleNotificationPermissionRequest = handleNotificationPermissionRequest,
saveRequestPermissionAgainPreference = saveRequestPermissionAgainPreference,
navigateToWorkoutScreen = {
navController.navigate(Route.WorkoutScreen(workoutId = workoutId)) {
@@ -82,13 +181,13 @@ fun RequestPermissionScreen(
)
}
-@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun RequestPermissionsScreenContent(
navController: NavHostController,
requestPermissionNextTime: Boolean,
hasNotificationPermission: Boolean,
- launchNotificationPermissionRequest: () -> Unit,
+ handleNotificationPermissionRequest: () -> Unit,
saveRequestPermissionAgainPreference: (Boolean) -> Unit,
navigateToWorkoutScreen: () -> Unit
) {
@@ -147,7 +246,7 @@ private fun RequestPermissionsScreenContent(
modifier = Modifier
.padding(start = 10.dp),
checked = hasNotificationPermission,
- onCheckedChange = { launchNotificationPermissionRequest() }
+ onCheckedChange = { handleNotificationPermissionRequest() }
)
}
}
@@ -235,7 +334,6 @@ private fun RequestPermissionsScreenContent(
}
}
-@OptIn(ExperimentalPermissionsApi::class)
@Preview
@Composable
private fun RequestPermissionsScreenPreview() {
@@ -245,7 +343,7 @@ private fun RequestPermissionsScreenPreview() {
navController = rememberNavController(),
requestPermissionNextTime = Random.nextBoolean(),
hasNotificationPermission = hasNotificationPermission.value,
- launchNotificationPermissionRequest = {
+ handleNotificationPermissionRequest = {
hasNotificationPermission.value = !hasNotificationPermission.value
},
saveRequestPermissionAgainPreference = {},
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bf8caef5c..2742f277e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -254,6 +254,8 @@
Don\'t ask again
Skip for now
Continue
+ Notifications are blocked. Please enable them in system settings to receive workout updates.
+ Open Settings
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 55800d96d..08a19be94 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,7 +8,6 @@ composeBom = "2026.06.01"
roomVersion = "2.8.4"
appcompat = "1.7.1"
ksp = "2.3.4"
-accompanistPermissions = "0.37.3"
lottieCompose = "6.7.1"
coreSplashscreen = "1.2.0"
composeM3 = "3.2.3"
@@ -48,7 +47,6 @@ navigation-compose = { module = "androidx.navigation:navigation-compose", versio
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.11.0" }
androidx-datastore-core-android = { group = "androidx.datastore", name = "datastore-core-android", version.ref = "datastore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
-accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistPermissions" }
lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottieCompose" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "composeM3" }