diff --git a/coreapp/src/androidMain/kotlin/com/metacto/core/di/CorePlatformModule.kt b/coreapp/src/androidMain/kotlin/com/metacto/core/di/CorePlatformModule.kt index 73e94d6b..433bbfac 100644 --- a/coreapp/src/androidMain/kotlin/com/metacto/core/di/CorePlatformModule.kt +++ b/coreapp/src/androidMain/kotlin/com/metacto/core/di/CorePlatformModule.kt @@ -6,6 +6,8 @@ import com.metacto.core.domain.repos.RepositoriesFactory import com.metacto.core.domain.repos.forceUpdate.ForceUpdateRepository import com.metacto.core.permissions.IPermissionManager import com.metacto.core.permissions.PermissionManager +import com.metacto.core.permissions.helpers.IPermissionFactory +import com.metacto.core.permissions.helpers.PermissionFactory import com.metacto.core.presentation.base.CommonViewModel import com.metacto.core.presentation.camera.CameraController import com.metacto.core.presentation.camera.CameraEngine @@ -84,8 +86,15 @@ actual fun corePlatformModule( ) } + single { + PermissionFactory() + } + single { - PermissionManager(androidContext()) + PermissionManager( + context = androidContext(), + permissionFactory = get() + ) } single { diff --git a/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/PermissionManager.kt b/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/PermissionManager.kt index c051ceea..744c7d99 100644 --- a/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/PermissionManager.kt +++ b/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/PermissionManager.kt @@ -18,8 +18,9 @@ import com.metacto.core.permissions.enums.PermissionState import com.metacto.core.permissions.exceptions.DeniedAlwaysException import com.metacto.core.permissions.exceptions.DeniedException import com.metacto.core.permissions.exceptions.RequestCanceledException -import com.metacto.core.permissions.helpers.toPlatformPermission +import com.metacto.core.permissions.helpers.IPermissionFactory import com.metacto.core.utils.extensions.openAppSettings + import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -29,7 +30,11 @@ import kotlinx.coroutines.withTimeoutOrNull import java.util.UUID import kotlin.coroutines.suspendCoroutine -internal class PermissionManager(private val context: Context) : IPermissionManager { +internal class PermissionManager( + private val context: Context, + private val permissionFactory: IPermissionFactory +) : IPermissionManager { + private val activityHolder = MutableStateFlow(null) private val launcherHolder = MutableStateFlow>?>(null) private var permissionCallback: PermissionCallback? = null @@ -112,7 +117,7 @@ internal class PermissionManager(private val context: Context) : IPermissionMana private suspend fun handlePermissionRequest(permission: Permission) { mutex.withLock { val launcher = awaitActivityResultLauncher() - val platformPermissions = permission.toPlatformPermission() + val platformPermissions = getAndroidPermissions(permission) suspendCoroutine { continuation -> permissionCallback = PermissionCallback(permission, continuation::resumeWith) @@ -133,6 +138,10 @@ internal class PermissionManager(private val context: Context) : IPermissionMana } } + private fun getAndroidPermissions(permission: Permission): List { + return permissionFactory.getPermissions(permission) + } + private suspend fun awaitActivityResultLauncher(): ActivityResultLauncher> { return launcherHolder.value ?: withTimeoutOrNull(AWAIT_ACTIVITY_TIMEOUT_MS) { launcherHolder.filterNotNull().first() @@ -155,7 +164,7 @@ internal class PermissionManager(private val context: Context) : IPermissionMana } private suspend fun getRuntimePermissionState(permission: Permission): PermissionState { - val permissions = permission.toPlatformPermission() + val permissions = getAndroidPermissions(permission) val status = permissions.map { ContextCompat.checkSelfPermission(context, it) } diff --git a/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/helpers/IPermissionFactory.kt b/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/helpers/IPermissionFactory.kt new file mode 100644 index 00000000..52f9b942 --- /dev/null +++ b/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/helpers/IPermissionFactory.kt @@ -0,0 +1,7 @@ +package com.metacto.core.permissions.helpers + +import com.metacto.core.permissions.enums.Permission + +interface IPermissionFactory { + fun getPermissions(permission: Permission): List +} \ No newline at end of file diff --git a/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/helpers/PermissionFactory.kt b/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/helpers/PermissionFactory.kt new file mode 100644 index 00000000..5b0cdb81 --- /dev/null +++ b/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/helpers/PermissionFactory.kt @@ -0,0 +1,155 @@ +package com.metacto.core.permissions.helpers + +import android.Manifest +import android.os.Build +import com.metacto.core.permissions.enums.Permission + +internal class PermissionFactory : IPermissionFactory { + + @Suppress("CyclomaticComplexMethod") + override fun getPermissions(permission: Permission): List { + return when (permission) { + Permission.CAMERA -> listOf(Manifest.permission.CAMERA) + Permission.GALLERY -> galleryCompat() + Permission.STORAGE -> allStoragePermissions() + Permission.WRITE_STORAGE -> writeStorageCompat() + Permission.LOCATION -> fineLocationCompat() + Permission.COARSE_LOCATION -> listOf(Manifest.permission.ACCESS_COARSE_LOCATION) + Permission.BACKGROUND_LOCATION -> backgroundLocationCompat() + Permission.REMOTE_NOTIFICATION -> remoteNotificationsPermissions() + Permission.RECORD_AUDIO -> listOf(Manifest.permission.RECORD_AUDIO) + Permission.BLUETOOTH_LE -> allBluetoothPermissions() + Permission.BLUETOOTH_SCAN -> bluetoothScanCompat() + Permission.BLUETOOTH_ADVERTISE -> bluetoothAdvertiseCompat() + Permission.BLUETOOTH_CONNECT -> bluetoothConnectCompat() + Permission.CONTACTS -> listOf( + Manifest.permission.READ_CONTACTS, + Manifest.permission.WRITE_CONTACTS + ) + Permission.MOTION -> motionPermissions() + Permission.CALENDER -> listOf( + Manifest.permission.READ_CALENDAR, + Manifest.permission.WRITE_CALENDAR + ) + } + } + + private fun writeStorageCompat(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // On Android 10+, this permission is ignored by the system + emptyList() + } else { + listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + + private fun allStoragePermissions(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + listOf( + Manifest.permission.READ_MEDIA_AUDIO, + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO + ) + } else { + listOf(Manifest.permission.READ_EXTERNAL_STORAGE) + } + } + + private fun galleryCompat(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + listOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO + ) + } else { + listOf(Manifest.permission.READ_EXTERNAL_STORAGE) + } + } + + private fun fineLocationCompat(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + listOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) + } else { + listOf(Manifest.permission.ACCESS_FINE_LOCATION) + } + } + + private fun backgroundLocationCompat(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + listOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_BACKGROUND_LOCATION, + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + listOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_BACKGROUND_LOCATION, + ) + } else { + listOf(Manifest.permission.ACCESS_FINE_LOCATION) + } + } + + private fun allBluetoothPermissions(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + listOf( + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_ADVERTISE, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + } else { + listOf( + Manifest.permission.BLUETOOTH, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + } + } + + private fun bluetoothScanCompat(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + listOf(Manifest.permission.BLUETOOTH_SCAN) + } else { + listOf(Manifest.permission.BLUETOOTH) + } + } + + private fun bluetoothAdvertiseCompat(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + listOf(Manifest.permission.BLUETOOTH_ADVERTISE) + } else { + listOf(Manifest.permission.BLUETOOTH) + } + } + + private fun bluetoothConnectCompat(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + listOf(Manifest.permission.BLUETOOTH_CONNECT) + } else { + listOf(Manifest.permission.BLUETOOTH) + } + } + + private fun motionPermissions(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + listOf( + Manifest.permission.ACTIVITY_RECOGNITION, + Manifest.permission.BODY_SENSORS + ) + } else { + listOf(Manifest.permission.BODY_SENSORS) + } + } + + private fun remoteNotificationsPermissions(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + listOf(Manifest.permission.POST_NOTIFICATIONS) + } else { + emptyList() + } + } +} \ No newline at end of file diff --git a/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/helpers/PermissionsMap.kt b/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/helpers/PermissionsMap.kt deleted file mode 100644 index eca8a45b..00000000 --- a/coreapp/src/androidMain/kotlin/com/metacto/core/permissions/helpers/PermissionsMap.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.metacto.core.permissions.helpers - -import android.Manifest -import android.os.Build -import com.metacto.core.permissions.enums.Permission - -@Suppress("CyclomaticComplexMethod") -internal fun Permission.toPlatformPermission(): List { - return when (this) { - Permission.CAMERA -> listOf(Manifest.permission.CAMERA) - Permission.GALLERY -> galleryCompat() - Permission.STORAGE -> allStoragePermissions() - Permission.WRITE_STORAGE -> listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) - Permission.LOCATION -> fineLocationCompat() - Permission.COARSE_LOCATION -> listOf(Manifest.permission.ACCESS_COARSE_LOCATION) - Permission.BACKGROUND_LOCATION -> backgroundLocationCompat() - Permission.REMOTE_NOTIFICATION -> remoteNotificationsPermissions() - Permission.RECORD_AUDIO -> listOf(Manifest.permission.RECORD_AUDIO) - Permission.BLUETOOTH_LE -> allBluetoothPermissions() - Permission.BLUETOOTH_SCAN -> bluetoothScanCompat() - Permission.BLUETOOTH_ADVERTISE -> bluetoothAdvertiseCompat() - Permission.BLUETOOTH_CONNECT -> bluetoothConnectCompat() - Permission.CONTACTS -> listOf( - Manifest.permission.READ_CONTACTS, - Manifest.permission.WRITE_CONTACTS - ) - - Permission.MOTION -> motionPermissions() - Permission.CALENDER -> listOf( - Manifest.permission.READ_CALENDAR, - Manifest.permission.WRITE_CALENDAR - ) - } -} - -private fun allStoragePermissions(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - listOf( - Manifest.permission.READ_MEDIA_AUDIO, - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO - ) - } else { - listOf(Manifest.permission.READ_EXTERNAL_STORAGE) - } -} - -private fun galleryCompat(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - listOf( - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO - ) - } else { - listOf(Manifest.permission.READ_EXTERNAL_STORAGE) - } -} - -private fun fineLocationCompat(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - listOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - ) - } else { - listOf(Manifest.permission.ACCESS_FINE_LOCATION) - } -} - -private fun backgroundLocationCompat(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - listOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_BACKGROUND_LOCATION, - ) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - listOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_BACKGROUND_LOCATION, - ) - } else { - listOf(Manifest.permission.ACCESS_FINE_LOCATION) - } -} - -private fun allBluetoothPermissions(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - listOf( - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.BLUETOOTH_SCAN, - Manifest.permission.BLUETOOTH_ADVERTISE, - Manifest.permission.ACCESS_COARSE_LOCATION - ) - } else { - listOf( - Manifest.permission.BLUETOOTH, - Manifest.permission.ACCESS_COARSE_LOCATION - ) - } -} - -private fun bluetoothScanCompat(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - listOf(Manifest.permission.BLUETOOTH_SCAN) - } else { - listOf(Manifest.permission.BLUETOOTH) - } - -} - -private fun bluetoothAdvertiseCompat(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - listOf(Manifest.permission.BLUETOOTH_ADVERTISE) - } else { - listOf(Manifest.permission.BLUETOOTH) - } -} - -private fun bluetoothConnectCompat(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - listOf(Manifest.permission.BLUETOOTH_CONNECT) - } else { - listOf(Manifest.permission.BLUETOOTH) - } -} - -private fun motionPermissions(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - listOf( - Manifest.permission.ACTIVITY_RECOGNITION, - Manifest.permission.BODY_SENSORS - ) - } else { - listOf(Manifest.permission.BODY_SENSORS) - } -} - -private fun remoteNotificationsPermissions(): List { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - listOf(Manifest.permission.POST_NOTIFICATIONS) - } else { - emptyList() - } -} \ No newline at end of file diff --git a/sampleAppShared/src/androidMain/AndroidManifest.xml b/sampleAppShared/src/androidMain/AndroidManifest.xml index 56df6478..08ad5d03 100644 --- a/sampleAppShared/src/androidMain/AndroidManifest.xml +++ b/sampleAppShared/src/androidMain/AndroidManifest.xml @@ -1,9 +1,41 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/di/ViewModelsModule.kt b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/di/ViewModelsModule.kt index 6e40f1b0..f8881897 100644 --- a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/di/ViewModelsModule.kt +++ b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/di/ViewModelsModule.kt @@ -7,6 +7,7 @@ import com.sampleApp.app.presentation.app.app.AppViewModel import com.sampleApp.app.presentation.camera.CameraViewModel import com.sampleApp.app.presentation.home.HomeViewModel import com.sampleApp.app.presentation.main.MainViewModel +import com.sampleApp.app.presentation.permissions.PermissionsTestViewModel import com.sampleApp.app.presentation.profile.ProfileViewModel import com.sampleApp.app.presentation.test.TestViewModel import com.sampleApp.app.presentation.testsheet1.TestSheet1ViewModel @@ -23,5 +24,6 @@ val viewModelsModule = module { commonViewModel { ProfileViewModel() } commonViewModel { HomeViewModel(get(),get(), get()) } commonViewModel { MainViewModel() } + commonViewModel { PermissionsTestViewModel() } single { AppViewModel() } } diff --git a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/HomeContract.kt b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/HomeContract.kt index 748b7b4d..4968f230 100644 --- a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/HomeContract.kt +++ b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/HomeContract.kt @@ -28,12 +28,12 @@ class HomeContract { data object Init : Event() data object NavToYoutubeScreen : Event() data object NavToTestScreen : Event() + data object NavToPermissionsTest : Event() data class ChangeCurrentVideo(val index: Int) : Event() data class VideoPlayerControllerCreated(val controller: VideoPlayerController) : Event() data object OpenPicker : Event() data class ImageFailedLoading(val error: ErrorResult) : Event() data object NavigateToCameraScreen : Event() - data object RequestCameraPermClicked : Event() } sealed class Effect : ViewSideEffect diff --git a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/HomeViewModel.kt b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/HomeViewModel.kt index 9f7d6b94..426aab13 100644 --- a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/HomeViewModel.kt +++ b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/HomeViewModel.kt @@ -20,6 +20,7 @@ import com.sampleApp.app.presentation.home.HomeContract.Effect import com.sampleApp.app.presentation.home.HomeContract.Event import com.sampleApp.app.presentation.home.HomeContract.State import com.sampleApp.app.presentation.test.TestScreen +import com.sampleApp.app.presentation.permissions.PermissionsTestScreen import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime @@ -43,6 +44,10 @@ class HomeViewModel( navManager.navigate(TestScreen) } + Event.NavToPermissionsTest -> { + navManager.navigate(PermissionsTestScreen) + } + is Event.ChangeCurrentVideo -> { setState { copy(currentVideo = VIDEOS_LIST[event.index]) } currentState.videoController?.play() @@ -70,32 +75,23 @@ class HomeViewModel( Event.NavigateToCameraScreen -> { globalState.messagePopup( params = - MessagePopupParams( - body = "These data give a picture of your physical health. Factors taken into account include how much you exercise and how much energy you burn while active.\n" + - "\n" + - "Vigorous, regular exercise is directly correlated with good physical health, and longer exercise sessions are strong indicators of endurance and overall fitness. ", - description = "Sources:\n" + - "Shaffer, F., & Ginsberg, J. P. (2017). An overview of heart rate variability metrics and norms. Frontiers in Public Health, 5, 258.\n" + - "Brosschot, J. F., van Dijk, E., & Thayer, J. F. (2007). Daily worry is related to low heart rate variability during waking and the subsequent nocturnal sleep period. International Journal of Psychophysiology, 63(1), 39-47." - ) + MessagePopupParams( + body = "These data give a picture of your physical health. Factors taken into account include how much you exercise and how much energy you burn while active.\n" + + "\n" + + "Vigorous, regular exercise is directly correlated with good physical health, and longer exercise sessions are strong indicators of endurance and overall fitness. ", + description = "Sources:\n" + + "Shaffer, F., & Ginsberg, J. P. (2017). An overview of heart rate variability metrics and norms. Frontiers in Public Health, 5, 258.\n" + + "Brosschot, J. F., van Dijk, E., & Thayer, J. F. (2007). Daily worry is related to low heart rate variability during waking and the subsequent nocturnal sleep period. International Journal of Psychophysiology, 63(1), 39-47." + ) ) } - Event.RequestCameraPermClicked -> { - executeSilent({ - permissionManager.requestPermission(Permission.CAMERA) - setState { copy(cameraPermState = PermissionState.Granted) } - }) - } - is Event.ImageFailedLoading -> onImageFailedLoading(event.error) } private fun init() { - // Validate if already initialized if (currentState.isInitialized) return - setState { copy(image = ImageUIModel(url = "https://scstage103-cd.joycemeyer.org/-/media/JoyceMeyer/Grow-Your-Faith/Daily-Devo/DevoImage17.jpg")) } val date1 = LocalDate(2021, 1, 1) @@ -119,7 +115,6 @@ class HomeViewModel( println("Formatted date ==== date4: ${date4.formatToRelativeDate()}") println("Formatted date ==== date5: ${date5.formatToRelativeDate()}") - // Init executeSilent({ val cameraPermState = permissionManager.getPermissionState(Permission.CAMERA) setState { @@ -134,7 +129,6 @@ class HomeViewModel( observeItemPickerResults() - // Update the flag setState { copy(isInitialized = true) } } @@ -149,8 +143,8 @@ class HomeViewModel( val url = currentState.image?.url ?: return@executeSilent errorResult.request.data if(error.contains("makeFromEncoded", true)) { - val bytes = uploadRepository.downloadBytes(url, errorResult.request.httpHeaders.asMap()) - setState { copy(image = image.copyOrCreate(bytes = bytes)) } + val bytes = uploadRepository.downloadBytes(url, errorResult.request.httpHeaders.asMap()) + setState { copy(image = image.copyOrCreate(bytes = bytes)) } } }) -} +} \ No newline at end of file diff --git a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/components/HomeContent.kt b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/components/HomeContent.kt index 727a7d6e..d7093f28 100644 --- a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/components/HomeContent.kt +++ b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/home/components/HomeContent.kt @@ -177,33 +177,33 @@ internal fun HomeContent( PrimaryFilledButton( modifier = Modifier.fillMaxWidth(), - text = "To Test Screen", + text = "Test Permissions System", onClick = { - onEvent(Event.NavToTestScreen) + onEvent(Event.NavToPermissionsTest) } ) PrimaryFilledButton( modifier = Modifier.fillMaxWidth(), - text = "To Test Screen 2", + text = "To Test Screen", onClick = { - navManager.navigate(Test2Screen()) + onEvent(Event.NavToTestScreen) } ) PrimaryFilledButton( modifier = Modifier.fillMaxWidth(), - text = "Open Picker", + text = "To Test Screen 2", onClick = { - onEvent(Event.OpenPicker) + navManager.navigate(Test2Screen()) } ) PrimaryFilledButton( modifier = Modifier.fillMaxWidth(), - text = "Request camera permissions", + text = "Open Picker", onClick = { - onEvent(Event.RequestCameraPermClicked) + onEvent(Event.OpenPicker) } ) diff --git a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestContent.kt b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestContent.kt new file mode 100644 index 00000000..9feea52e --- /dev/null +++ b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestContent.kt @@ -0,0 +1,138 @@ +package com.sampleApp.app.presentation.permissions + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import com.metacto.core.permissions.enums.Permission +import com.metacto.core.permissions.enums.PermissionState +import com.metacto.core.presentation.components.buttons.PrimaryFilledButton +import com.sampleApp.app.presentation.permissions.models.PermissionDetailData +import com.sampleApp.app.presentation.permissions.models.PermissionDetailsProvider +import com.sampleApp.app.presentation.theme.AppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun PermissionsTestContent( + permissionStates: Map, + isLoading: Boolean, + onBackClicked: () -> Unit, + onRequestPermission: (Permission, Boolean) -> Unit, +) { + val permissionDetails = remember { PermissionDetailsProvider.getAllPermissionDetails() } + + Column(modifier = Modifier.fillMaxSize()) { + TopAppBar( + title = { Text("Permissions Test") }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon(Icons.Default.ArrowBack, contentDescription = "Back") + } + } + ) + + if (isLoading) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(AppTheme.spacings.spacing16), + verticalArrangement = Arrangement.spacedBy(AppTheme.spacings.spacing12) + ) { + + items(permissionDetails) { permissionDetail -> + SimplePermissionCard( + permissionDetail = permissionDetail, + currentState = permissionStates[permissionDetail.permission] ?: PermissionState.NotDetermined, + isLoading = isLoading , + onRequestPermission = { + onRequestPermission(permissionDetail.permission, true) + } + ) + } + } + } +} + +@Composable +private fun SimplePermissionCard( + permissionDetail: PermissionDetailData, + currentState: PermissionState, + isLoading: Boolean, + onRequestPermission: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = AppTheme.spacings.spacing4) + ) { + Column(modifier = Modifier.padding(AppTheme.spacings.spacing16)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = permissionDetail.displayName, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + + PermissionStateChip(currentState) + } + + if (isLoading) { + Spacer(modifier = Modifier.height(AppTheme.spacings.spacing8)) + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + + Spacer(modifier = Modifier.height(AppTheme.spacings.spacing12)) + + PrimaryFilledButton( + modifier = Modifier.fillMaxWidth(), + text = "Request Permission", + onClick = onRequestPermission + ) + } + } +} + +@Composable +private fun PermissionStateChip(state: PermissionState) { + val (color, icon, text) = when (state) { + PermissionState.Granted -> Triple(AppTheme.colors.terra, Icons.Default.Check, "Granted") + PermissionState.Denied -> Triple(AppTheme.colors.nectarineDark, Icons.Default.Warning, "Denied") + PermissionState.DeniedAlways -> Triple(AppTheme.colors.terra, Icons.Default.Close, "Denied Always") + PermissionState.NotDetermined -> Triple(AppTheme.colors.gray, Icons.Default.Help, "Unknown") + } + + Row( + modifier = Modifier + .background(color = color.copy(alpha = 0.1f), shape = AppTheme.shapes.roundedCorner12) + .padding(horizontal = AppTheme.spacings.spacing8, vertical = AppTheme.spacings.spacing4), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(AppTheme.spacings.spacing4) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = color, + modifier = Modifier.size(AppTheme.spacings.spacing16) + ) + Text( + text = text, + color = color, + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold + ) + } +} \ No newline at end of file diff --git a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestContract.kt b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestContract.kt new file mode 100644 index 00000000..bdbe1262 --- /dev/null +++ b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestContract.kt @@ -0,0 +1,37 @@ +package com.sampleApp.app.presentation.permissions + +import com.metacto.core.permissions.enums.Permission +import com.metacto.core.permissions.enums.PermissionState +import com.metacto.core.presentation.base.ViewEvent +import com.metacto.core.presentation.base.ViewSideEffect +import com.metacto.core.presentation.base.ViewState + + +class PermissionsTestContract { + + data class State( + val isInitialized: Boolean = false, + val permissionStates: Map = emptyMap(), + val isLoading: Boolean = false, + val lastResult: String = "", + val testResults: List = emptyList() + ) : ViewState + + sealed class Event : ViewEvent { + data object Init : Event() + data object RefreshAllPermissions : Event() + data class RequestPermission(val permission: Permission, val openAppSettings: Boolean = true) : Event() + data class CheckPermissionState(val permission: Permission) : Event() + data object ClearResults : Event() + data object TestAllPermissions : Event() + data object BackClicked : Event() + } + + sealed class Effect : ViewSideEffect + + data class TestResult( + val permission: Permission, + val action: String, + val result: String, + ) +} \ No newline at end of file diff --git a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestScreen.kt b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestScreen.kt new file mode 100644 index 00000000..fd2805b0 --- /dev/null +++ b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestScreen.kt @@ -0,0 +1,36 @@ +package com.sampleApp.app.presentation.permissions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.metacto.core.permissions.BindEffect +import com.metacto.core.permissions.IPermissionManager +import com.metacto.core.presentation.base.BaseScreen +import com.metacto.core.presentation.base.SIDE_EFFECTS_KEY +import com.metacto.core.presentation.base.rememberViewModel + +import com.sampleApp.app.presentation.permissions.PermissionsTestContract.Event +import org.koin.compose.koinInject + +internal object PermissionsTestScreen : BaseScreen() { + @Composable + override fun Content() { + val viewModel = rememberViewModel() + val permissionManager = koinInject() + val state = viewModel.viewState.value + + BindEffect(permissionManager) + + LaunchedEffect(SIDE_EFFECTS_KEY) { + viewModel.setEvent(Event.Init) + } + + PermissionsTestContent( + permissionStates = state.permissionStates, + isLoading = state.isLoading, + onBackClicked = { viewModel.setEvent(Event.BackClicked) }, + onRequestPermission = { permission, openSettings -> + viewModel.setEvent(Event.RequestPermission(permission, openSettings)) + } + ) + } +} \ No newline at end of file diff --git a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestViewModel.kt b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestViewModel.kt new file mode 100644 index 00000000..c3c7641d --- /dev/null +++ b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/PermissionsTestViewModel.kt @@ -0,0 +1,234 @@ +package com.sampleApp.app.presentation.permissions + +import com.metacto.core.permissions.enums.Permission +import com.metacto.core.permissions.enums.PermissionState +import com.metacto.core.permissions.exceptions.DeniedAlwaysException +import com.metacto.core.permissions.exceptions.DeniedException +import com.metacto.core.permissions.exceptions.RequestCanceledException +import com.metacto.core.presentation.globalState.models.SnackBarParams +import com.metacto.core.presentation.globalState.models.SnackBarType +import com.sampleApp.app.presentation.base.BaseViewModel +import com.sampleApp.app.presentation.permissions.PermissionsTestContract.Event +import com.sampleApp.app.presentation.permissions.PermissionsTestContract.State +import com.sampleApp.app.presentation.permissions.PermissionsTestContract.Effect +import com.sampleApp.app.presentation.permissions.PermissionsTestContract.TestResult +import kotlinx.coroutines.delay + +class PermissionsTestViewModel : BaseViewModel() { + + override fun setInitialState() = State() + + override fun handleEvents(event: Event): Any = when (event) { + Event.Init -> init() + Event.RefreshAllPermissions -> refreshAllPermissions() + is Event.RequestPermission -> requestPermission(event.permission, event.openAppSettings) + is Event.CheckPermissionState -> checkPermissionState(event.permission) + Event.ClearResults -> clearResults() + Event.TestAllPermissions -> testAllPermissions() + Event.BackClicked -> navManager.goBack() + } + + private fun init() { + if (currentState.isInitialized) return + setState { copy(isInitialized = true) } + refreshAllPermissions() + } + + private fun refreshAllPermissions() = executeSilent({ + setState { copy(isLoading = true) } + + val permissionStates = mutableMapOf() + + Permission.entries.forEach { permission -> + try { + val state = permissionManager.getPermissionState(permission) + permissionStates[permission] = state + delay(50) + } catch (e: Exception) { + permissionStates[permission] = PermissionState.NotDetermined + addTestResult(permission, "Get State", "Error: ${e.message}") + } + } + + setState { + copy( + permissionStates = permissionStates, + isLoading = false + ) + } + + globalState.snackBar( + SnackBarParams( + message = "Refreshed all permission states", + type = SnackBarType.SUCCESS + ) + ) + }) + + private fun requestPermission(permission: Permission, openAppSettings: Boolean) = executeSilent({ + setState { copy(isLoading = true) } + + try { + permissionManager.requestPermission(permission, openAppSettings) + + val newState = permissionManager.getPermissionState(permission) + val updatedStates = currentState.permissionStates.toMutableMap() + updatedStates[permission] = newState + + setState { + copy( + permissionStates = updatedStates, + lastResult = "✅ Permission granted", + isLoading = false + ) + } + + addTestResult(permission, "Request (settings: $openAppSettings)", "Granted") + + globalState.snackBar( + SnackBarParams( + message = "✅ ${permission.name} permission granted", + type = SnackBarType.SUCCESS + ) + ) + + } catch (e: DeniedAlwaysException) { + val updatedStates = currentState.permissionStates.toMutableMap() + updatedStates[permission] = PermissionState.DeniedAlways + + setState { + copy( + permissionStates = updatedStates, + lastResult = "❌ Permission denied always", + isLoading = false + ) + } + + addTestResult(permission, "Request (settings: $openAppSettings)", "Denied Always") + + globalState.snackBar( + SnackBarParams( + message = "❌ ${permission.name} permission denied always", + type = SnackBarType.ERROR + ) + ) + + } catch (e: DeniedException) { + val updatedStates = currentState.permissionStates.toMutableMap() + updatedStates[permission] = PermissionState.Denied + + setState { + copy( + permissionStates = updatedStates, + lastResult = "⚠️ Permission denied", + isLoading = false + ) + } + + addTestResult(permission, "Request (settings: $openAppSettings)", "Denied") + + globalState.snackBar( + SnackBarParams( + message = "⚠️ ${permission.name} permission denied", + type = SnackBarType.ERROR + ) + ) + + } catch (e: RequestCanceledException) { + setState { + copy( + lastResult = "❌ Request cancelled", + isLoading = false + ) + } + + addTestResult(permission, "Request (settings: $openAppSettings)", "Cancelled") + + globalState.snackBar( + SnackBarParams( + message = "❌ ${permission.name} request cancelled", + type = SnackBarType.ERROR + ) + ) + + } catch (e: Exception) { + setState { + copy( + lastResult = "❌ Error: ${e.message}", + isLoading = false + ) + } + + addTestResult(permission, "Request (settings: $openAppSettings)", "Error: ${e.message}") + + globalState.snackBar( + SnackBarParams( + message = "❌ Error requesting ${permission.name}: ${e.message}", + type = SnackBarType.ERROR + ) + ) + } + }) + + private fun checkPermissionState(permission: Permission) = executeSilent({ + val state = permissionManager.getPermissionState(permission) + val isGranted = permissionManager.isPermissionGranted(permission) + + val updatedStates = currentState.permissionStates.toMutableMap() + updatedStates[permission] = state + + setState { + copy( + permissionStates = updatedStates, + lastResult = "State: $state, Granted: $isGranted" + ) + } + + addTestResult(permission, "Check State", "State: $state, Granted: $isGranted") + }) + + private fun testAllPermissions() = executeSilent({ + setState { copy(isLoading = true) } + + Permission.entries.forEach { permission -> + try { + val state = permissionManager.getPermissionState(permission) + val isGranted = permissionManager.isPermissionGranted(permission) + + addTestResult( + permission, + "Auto Test", + "State: $state, Granted: $isGranted" + ) + + delay(100) + } catch (e: Exception) { + addTestResult(permission, "Auto Test", "Error: ${e.message}") + } + } + + setState { copy(isLoading = false) } + + globalState.snackBar( + SnackBarParams( + message = "Completed testing all permissions", + type = SnackBarType.SUCCESS + ) + ) + }) + + private fun clearResults() { + setState { + copy( + testResults = emptyList(), + lastResult = "" + ) + } + } + + private fun addTestResult(permission: Permission, action: String, result: String) { + val newResult = TestResult(permission, action, result) + val updatedResults = currentState.testResults + newResult + setState { copy(testResults = updatedResults) } + } +} \ No newline at end of file diff --git a/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/models/PermissionDetailData.kt b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/models/PermissionDetailData.kt new file mode 100644 index 00000000..9df92088 --- /dev/null +++ b/sampleAppShared/src/commonMain/kotlin/com/sampleApp/app/presentation/permissions/models/PermissionDetailData.kt @@ -0,0 +1,184 @@ +package com.sampleApp.app.presentation.permissions.models + +import com.metacto.core.permissions.enums.Permission + + +data class PermissionDetailData( + val permission: Permission, + val displayName: String, + val description: String, + val androidPermissions: List, + val iosEquivalent: String, + val useCases: List, + val requiredForFeatures: List +) + +object PermissionDetailsProvider { + fun getPermissionDetails(permission: Permission): PermissionDetailData { + return when (permission) { + Permission.CAMERA -> PermissionDetailData( + permission = Permission.CAMERA, + displayName = "📷 Camera", + description = "Access device camera to take photos and record videos", + androidPermissions = listOf("android.permission.CAMERA"), + iosEquivalent = "NSCameraUsageDescription", + useCases = listOf("Take profile photos", "Record videos", "Scan QR codes", "Document scanning"), + requiredForFeatures = listOf("Camera Screen", "Profile Photo", "Video Recording") + ) + + Permission.GALLERY -> PermissionDetailData( + permission = Permission.GALLERY, + displayName = "🖼️ Photo Gallery", + description = "Access photos and videos from device gallery", + androidPermissions = listOf("READ_MEDIA_IMAGES (API 33+)", "READ_MEDIA_VIDEO (API 33+)", "READ_EXTERNAL_STORAGE (< API 33)"), + iosEquivalent = "NSPhotoLibraryUsageDescription", + useCases = listOf("Select profile photos", "Share media content", "Upload images", "Media picker"), + requiredForFeatures = listOf("Image Picker", "Media Upload", "Profile Photos") + ) + + Permission.STORAGE -> PermissionDetailData( + permission = Permission.STORAGE, + displayName = "💾 Storage Access", + description = "Read files from device storage", + androidPermissions = listOf("READ_MEDIA_AUDIO (API 33+)", "READ_MEDIA_IMAGES (API 33+)", "READ_MEDIA_VIDEO (API 33+)", "READ_EXTERNAL_STORAGE (< API 33)"), + iosEquivalent = "File access via DocumentPicker", + useCases = listOf("Access downloaded files", "Read media files", "File management", "Document access"), + requiredForFeatures = listOf("File Manager", "Media Access", "Document Viewer") + ) + + Permission.WRITE_STORAGE -> PermissionDetailData( + permission = Permission.WRITE_STORAGE, + displayName = "✏️ Write Storage", + description = "Write files to device storage", + androidPermissions = listOf("WRITE_EXTERNAL_STORAGE"), + iosEquivalent = "Files app integration", + useCases = listOf("Save downloaded files", "Export data", "Cache management", "File creation"), + requiredForFeatures = listOf("Download Manager", "Export Features", "File Creation") + ) + + Permission.LOCATION -> PermissionDetailData( + permission = Permission.LOCATION, + displayName = "📍 Precise Location", + description = "Access precise device location using GPS", + androidPermissions = listOf("ACCESS_FINE_LOCATION", "ACCESS_COARSE_LOCATION (API 31+)"), + iosEquivalent = "NSLocationWhenInUseUsageDescription", + useCases = listOf("Navigation", "Location-based services", "Weather by location", "Nearby search"), + requiredForFeatures = listOf("Maps", "Location Services", "Weather", "Check-in") + ) + + Permission.COARSE_LOCATION -> PermissionDetailData( + permission = Permission.COARSE_LOCATION, + displayName = "📍 Approximate Location", + description = "Access approximate device location", + androidPermissions = listOf("ACCESS_COARSE_LOCATION"), + iosEquivalent = "NSLocationWhenInUseUsageDescription (reduced accuracy)", + useCases = listOf("City-level location", "Timezone detection", "Regional content", "Approximate weather"), + requiredForFeatures = listOf("Regional Content", "Basic Location Services") + ) + + Permission.BACKGROUND_LOCATION -> PermissionDetailData( + permission = Permission.BACKGROUND_LOCATION, + displayName = "📍 Background Location", + description = "Access location when app is in background", + androidPermissions = listOf("ACCESS_FINE_LOCATION", "ACCESS_COARSE_LOCATION (API 31+)", "ACCESS_BACKGROUND_LOCATION"), + iosEquivalent = "NSLocationAlwaysAndWhenInUseUsageDescription", + useCases = listOf("Fitness tracking", "Location history", "Geofencing", "Navigation apps"), + requiredForFeatures = listOf("Fitness Tracking", "Geofencing", "Location History") + ) + + Permission.BLUETOOTH_LE -> PermissionDetailData( + permission = Permission.BLUETOOTH_LE, + displayName = "📶 Bluetooth (All)", + description = "Full Bluetooth access including scan, connect, and advertise", + androidPermissions = listOf("BLUETOOTH_CONNECT (API 31+)", "BLUETOOTH_SCAN (API 31+)", "BLUETOOTH_ADVERTISE (API 31+)", "ACCESS_COARSE_LOCATION", "BLUETOOTH (< API 31)"), + iosEquivalent = "NSBluetoothAlwaysUsageDescription", + useCases = listOf("Connect to devices", "Smart device control", "Fitness trackers", "Audio devices"), + requiredForFeatures = listOf("Device Pairing", "Smart Home", "Fitness Devices") + ) + + Permission.REMOTE_NOTIFICATION -> PermissionDetailData( + permission = Permission.REMOTE_NOTIFICATION, + displayName = "🔔 Push Notifications", + description = "Show push notifications from remote servers", + androidPermissions = listOf("POST_NOTIFICATIONS (API 33+)"), + iosEquivalent = "Push notification entitlement", + useCases = listOf("App updates", "Messages", "Alerts", "Marketing notifications"), + requiredForFeatures = listOf("Push Messaging", "App Notifications", "Alerts") + ) + + Permission.RECORD_AUDIO -> PermissionDetailData( + permission = Permission.RECORD_AUDIO, + displayName = "🎤 Microphone", + description = "Record audio using device microphone", + androidPermissions = listOf("RECORD_AUDIO"), + iosEquivalent = "NSMicrophoneUsageDescription", + useCases = listOf("Voice messages", "Audio recording", "Voice calls", "Speech recognition"), + requiredForFeatures = listOf("Voice Recording", "Video with Audio", "Voice Calls") + ) + + Permission.BLUETOOTH_SCAN -> PermissionDetailData( + permission = Permission.BLUETOOTH_SCAN, + displayName = "📶 Bluetooth Scan", + description = "Scan for nearby Bluetooth devices", + androidPermissions = listOf("BLUETOOTH_SCAN (API 31+)", "BLUETOOTH (< API 31)"), + iosEquivalent = "Core Bluetooth scanning", + useCases = listOf("Device discovery", "Nearby device detection", "Device pairing setup"), + requiredForFeatures = listOf("Device Discovery", "Bluetooth Setup") + ) + + Permission.BLUETOOTH_ADVERTISE -> PermissionDetailData( + permission = Permission.BLUETOOTH_ADVERTISE, + displayName = "📶 Bluetooth Advertise", + description = "Make device discoverable via Bluetooth", + androidPermissions = listOf("BLUETOOTH_ADVERTISE (API 31+)", "BLUETOOTH (< API 31)"), + iosEquivalent = "Core Bluetooth peripheral mode", + useCases = listOf("Device sharing", "Peer-to-peer connection", "Act as Bluetooth peripheral"), + requiredForFeatures = listOf("Device Sharing", "P2P Features") + ) + + Permission.BLUETOOTH_CONNECT -> PermissionDetailData( + permission = Permission.BLUETOOTH_CONNECT, + displayName = "📶 Bluetooth Connect", + description = "Connect to paired Bluetooth devices", + androidPermissions = listOf("BLUETOOTH_CONNECT (API 31+)", "BLUETOOTH (< API 31)"), + iosEquivalent = "Core Bluetooth central mode", + useCases = listOf("Connect to headphones", "Smart device control", "Data transfer"), + requiredForFeatures = listOf("Audio Devices", "Smart Device Control") + ) + + Permission.CONTACTS -> PermissionDetailData( + permission = Permission.CONTACTS, + displayName = "👥 Contacts", + description = "Access device contacts for reading and writing", + androidPermissions = listOf("READ_CONTACTS", "WRITE_CONTACTS"), + iosEquivalent = "NSContactsUsageDescription", + useCases = listOf("Friend invitations", "Contact sharing", "Auto-fill phone numbers", "Social features"), + requiredForFeatures = listOf("Contact Picker", "Friend Invites", "Social Features") + ) + + Permission.MOTION -> PermissionDetailData( + permission = Permission.MOTION, + displayName = "🏃 Motion & Fitness", + description = "Access motion and fitness sensors", + androidPermissions = listOf("ACTIVITY_RECOGNITION (API 31+)", "BODY_SENSORS"), + iosEquivalent = "NSMotionUsageDescription", + useCases = listOf("Step counting", "Activity tracking", "Fitness monitoring", "Health data"), + requiredForFeatures = listOf("Fitness Tracking", "Health Monitoring", "Step Counter") + ) + + Permission.CALENDER -> PermissionDetailData( + permission = Permission.CALENDER, + displayName = "📅 Calendar", + description = "Access device calendar for reading and writing events", + androidPermissions = listOf("READ_CALENDAR", "WRITE_CALENDAR"), + iosEquivalent = "NSCalendarsUsageDescription", + useCases = listOf("Event scheduling", "Meeting integration", "Reminder creation", "Calendar sync"), + requiredForFeatures = listOf("Event Scheduling", "Meeting Features", "Reminders") + ) + } + } + + fun getAllPermissionDetails(): List { + return Permission.entries.map { getPermissionDetails(it) } + } +} \ No newline at end of file