diff --git a/PermissionManager/build.gradle.kts b/PermissionManager/build.gradle.kts new file mode 100644 index 000000000..b85ce67e1 --- /dev/null +++ b/PermissionManager/build.gradle.kts @@ -0,0 +1,42 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(core.plugins.android.library) + alias(core.plugins.kotlin.android) + alias(core.plugins.compose.compiler) +} + +val coreCompileSdk: Int by rootProject.extra +val coreMinSdk: Int by rootProject.extra +val javaVersion: JavaVersion by rootProject.extra + +android { + namespace = "com.infomaniak.core.permissionmanager" + compileSdk = coreCompileSdk + + defaultConfig { + minSdk = coreMinSdk + } + + compileOptions { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + } + + buildFeatures { + compose = true + } + + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.fromTarget(javaVersion.toString())) + } + } +} + +dependencies { + implementation(platform(core.compose.bom)) + implementation(core.compose.runtime) + implementation(core.compose.ui) + implementation(core.accompanist.permissions) +} diff --git a/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/PermissionManagerState.kt b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/PermissionManagerState.kt new file mode 100644 index 000000000..e8e47e0bc --- /dev/null +++ b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/PermissionManagerState.kt @@ -0,0 +1,45 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.permissionmanager + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun rememberPermissionManagerState(permissionType: PermissionType): PermissionManagerState { + val permission = permissionType.permission + + return if (permission == null) { + remember { UnsupportedApiPermissionManagerState } + } else { + val permissionState = rememberPermissionState(permission) + remember(permission) { SupportedApiPermissionManagerState(permissionState) } + } +} + +@Stable +sealed interface PermissionManagerState { + fun launchPermissionRequestIfNeeded() + + @Composable + fun dropIfDenied(action: () -> Unit): () -> Unit +} diff --git a/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/PermissionType.kt b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/PermissionType.kt new file mode 100644 index 000000000..71fd665ed --- /dev/null +++ b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/PermissionType.kt @@ -0,0 +1,26 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.permissionmanager + +import android.Manifest +import android.os.Build.VERSION.SDK_INT + +enum class PermissionType(val permission: String?) { + Notification(permission = if (SDK_INT >= 33) Manifest.permission.POST_NOTIFICATIONS else null), + WriteExternalStorage(permission = if (SDK_INT >= 29) null else Manifest.permission.WRITE_EXTERNAL_STORAGE), +} diff --git a/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/SupportedApiPermissionManagerState.kt b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/SupportedApiPermissionManagerState.kt new file mode 100644 index 000000000..36fb6c8b2 --- /dev/null +++ b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/SupportedApiPermissionManagerState.kt @@ -0,0 +1,62 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.permissionmanager + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted +import kotlinx.coroutines.CompletableJob +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first + +@Stable +@OptIn(ExperimentalPermissionsApi::class) +internal class SupportedApiPermissionManagerState( + private val permissionState: PermissionState, +) : PermissionManagerState { + override fun launchPermissionRequestIfNeeded() { + if (permissionState.status.isGranted) return + permissionState.launchPermissionRequest() + } + + @Composable + override fun dropIfDenied(action: () -> Unit): () -> Unit = with(permissionState) { + val isGrantedFlow = remember(permission) { snapshotFlow { status.isGranted } } + val actionFired: CompletableJob = remember(permission) { Job() } + + LaunchedEffect(permission) { + actionFired.join() + isGrantedFlow.first { it } + action() + } + + return fun() { + if (status.isGranted) { + action() + } else { + launchPermissionRequest() + actionFired.complete() + } + } + } +} diff --git a/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/UnsupportedApiPermissionManagerState.kt b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/UnsupportedApiPermissionManagerState.kt new file mode 100644 index 000000000..d92d762ea --- /dev/null +++ b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/UnsupportedApiPermissionManagerState.kt @@ -0,0 +1,29 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.permissionmanager + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable + +@Stable +internal object UnsupportedApiPermissionManagerState : PermissionManagerState { + override fun launchPermissionRequestIfNeeded() = Unit + + @Composable + override fun dropIfDenied(action: () -> Unit): () -> Unit = action +} diff --git a/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/rationale/RationalePermissionManagerState.kt b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/rationale/RationalePermissionManagerState.kt new file mode 100644 index 000000000..6b96f0958 --- /dev/null +++ b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/rationale/RationalePermissionManagerState.kt @@ -0,0 +1,49 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.permissionmanager.rationale + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState +import com.infomaniak.core.permissionmanager.PermissionType + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun rememberRationalePermissionManagerState(permissionType: PermissionType): RationalePermissionManagerState { + val permission = permissionType.permission + + return if (permission == null) { + remember { UnsupportedApiRationalePermissionManagerState } + } else { + val permissionState = rememberPermissionState(permission) + val shouldShowRationale = rememberSaveable(permission) { mutableStateOf(false) } + remember(permission) { SupportedApiRationalePermissionManagerState(permissionState, shouldShowRationale) } + } +} + +@Stable +sealed interface RationalePermissionManagerState { + val shouldShowRationale: Boolean + + fun requestPermissionOrShowRationaleIfNeeded() + fun dismissAndAskPermission() +} diff --git a/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/rationale/SupportedApiRationalePermissionManagerState.kt b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/rationale/SupportedApiRationalePermissionManagerState.kt new file mode 100644 index 000000000..2c4f32a51 --- /dev/null +++ b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/rationale/SupportedApiRationalePermissionManagerState.kt @@ -0,0 +1,50 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.permissionmanager.rationale + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.shouldShowRationale + +@Stable +@OptIn(ExperimentalPermissionsApi::class) +internal class SupportedApiRationalePermissionManagerState( + private val permissionState: PermissionState, + private val _shouldShowRationale: MutableState, +) : RationalePermissionManagerState { + override val shouldShowRationale: Boolean by _shouldShowRationale + + override fun requestPermissionOrShowRationaleIfNeeded() { + if (permissionState.status.isGranted) return + + if (permissionState.status.shouldShowRationale) { + _shouldShowRationale.value = true + } else { + permissionState.launchPermissionRequest() + } + } + + override fun dismissAndAskPermission() { + _shouldShowRationale.value = false + permissionState.launchPermissionRequest() + } +} diff --git a/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/rationale/UnsupportedApiRationalePermissionManagerState.kt b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/rationale/UnsupportedApiRationalePermissionManagerState.kt new file mode 100644 index 000000000..ffb038a3b --- /dev/null +++ b/PermissionManager/src/main/kotlin/com/infomaniak/core/permissionmanager/rationale/UnsupportedApiRationalePermissionManagerState.kt @@ -0,0 +1,28 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.permissionmanager.rationale + +import androidx.compose.runtime.Stable + +@Stable +internal object UnsupportedApiRationalePermissionManagerState : RationalePermissionManagerState { + override val shouldShowRationale: Boolean get() = false + + override fun requestPermissionOrShowRationaleIfNeeded() = Unit + override fun dismissAndAskPermission() = Unit +} diff --git a/gradle/core.versions.toml b/gradle/core.versions.toml index 512a06d89..390675476 100644 --- a/gradle/core.versions.toml +++ b/gradle/core.versions.toml @@ -1,4 +1,5 @@ [versions] +accompanistPermissions = "0.37.3" activityCompose = "1.10.1" # TODO: Update to 1.11.0 or later after we bump compileSdk to 36 adaptive = "1.2.0" agp = "8.13.0" # Delete this to-do on agp 9+ after searching and checking the project for the following: TODO[AGP9 @@ -59,6 +60,7 @@ swipeRefreshLayout = "1.2.0" workManager = "2.11.0" [libraries] +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } android-login = { module = "com.github.infomaniak:android-login", version.ref = "androidLogin" } androidx-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" } @@ -141,6 +143,7 @@ infomaniak-core-network-ktor = { module = "com.infomaniak.core:Network.Ktor" } infomaniak-core-notifications = { module = "com.infomaniak.core:Notifications" } infomaniak-core-notifications-registration = { module = "com.infomaniak.core:Notifications.Registration" } infomaniak-core-onboarding = { module = "com.infomaniak.core:Onboarding" } +infomaniak-core-permissionmanager = { module = "com.infomaniak.core:PermissionManager" } infomaniak-core-privacymanagement = { module = "com.infomaniak.core:PrivacyManagement" } infomaniak-core-recyclerview = { module = "com.infomaniak.core:RecyclerView" } infomaniak-core-sentry = { module = "com.infomaniak.core:Sentry" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9cf95d0cf..7be4eb6ab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -56,6 +56,7 @@ include( ":Notifications", ":Notifications:Registration", ":Onboarding", + ":PermissionManager", ":PrivacyManagement", ":RecyclerView", ":Sentry",