diff --git a/CLAUDE.md b/CLAUDE.md index f405c1b..00c67af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,6 +99,5 @@ Bitmaps emitted to `StateFlow`s are **never eagerly recycled** immediately after ## Known limitations / future work - `minSdk = 35` (Android 15) — intentional for personal use. Lower to 26-29 if distributing. -- Accompanist Permissions (`0.36.0`) is deprecated; should migrate to first-party `activity-compose` API. - Dependencies are pinned to late-2024 versions; periodic bumps recommended. - Fragment shader uses `int` uniform branching in GLSL ES 1.00 — works but could be cleaner with ES 3.00. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5e3c123..450cc7b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -112,9 +112,6 @@ dependencies { // Location implementation(libs.play.services.location) - // Permissions - implementation(libs.accompanist.permissions) - // Debug debugImplementation(libs.androidx.ui.tooling) } diff --git a/app/src/main/java/no/naiv/tiltshift/MainActivity.kt b/app/src/main/java/no/naiv/tiltshift/MainActivity.kt index ab0cca3..7a379a2 100644 --- a/app/src/main/java/no/naiv/tiltshift/MainActivity.kt +++ b/app/src/main/java/no/naiv/tiltshift/MainActivity.kt @@ -2,12 +2,15 @@ package no.naiv.tiltshift import android.Manifest import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle import android.provider.Settings import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -26,23 +29,24 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +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.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberMultiplePermissionsState -import com.google.accompanist.permissions.rememberPermissionState -import com.google.accompanist.permissions.shouldShowRationale import no.naiv.tiltshift.ui.CameraScreen import no.naiv.tiltshift.ui.theme.AppColors @@ -66,21 +70,47 @@ class MainActivity : ComponentActivity() { } } -@OptIn(ExperimentalPermissionsApi::class) @Composable private fun TiltShiftApp() { - val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA) - val locationPermissions = rememberMultiplePermissionsState( - listOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION + val context = LocalContext.current + val activity = context as? ComponentActivity + + var cameraGranted by remember { + mutableStateOf( + ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED ) - ) + } + var locationGranted by remember { + mutableStateOf( + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) + == PackageManager.PERMISSION_GRANTED + ) + } + + // Track whether the camera permission dialog has returned a result, + // so we can distinguish "never asked" from "permanently denied" + var cameraResultReceived by remember { mutableStateOf(false) } + + val cameraPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + cameraGranted = granted + cameraResultReceived = true + } + + val locationPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + locationGranted = permissions.values.any { it } + } // Request camera permission on launch LaunchedEffect(Unit) { - if (!cameraPermission.status.isGranted) { - cameraPermission.launchPermissionRequest() + if (!cameraGranted) { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) } } @@ -90,28 +120,47 @@ private fun TiltShiftApp() { .background(Color.Black) ) { when { - cameraPermission.status.isGranted -> { + cameraGranted -> { // Camera permission granted - show camera CameraScreen() // Request location in background (for EXIF GPS) LaunchedEffect(Unit) { - if (!locationPermissions.allPermissionsGranted) { - locationPermissions.launchMultiplePermissionRequest() + if (!locationGranted) { + locationPermissionLauncher.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) } } } else -> { - // Permanently denied: not granted AND rationale not shown - val cameraPermanentlyDenied = !cameraPermission.status.isGranted && - !cameraPermission.status.shouldShowRationale + // Permanently denied: user has responded to the dialog, but permission + // is still denied and the system won't show the dialog again + val cameraPermanentlyDenied = cameraResultReceived && + activity?.let { + !ActivityCompat.shouldShowRequestPermissionRationale( + it, Manifest.permission.CAMERA + ) + } ?: false // Show permission request UI PermissionRequestScreen( - onRequestCamera = { cameraPermission.launchPermissionRequest() }, - onRequestLocation = { locationPermissions.launchMultiplePermissionRequest() }, - cameraGranted = cameraPermission.status.isGranted, - locationGranted = locationPermissions.allPermissionsGranted, + onRequestCamera = { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) + }, + onRequestLocation = { + locationPermissionLauncher.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) + }, + cameraGranted = false, + locationGranted = locationGranted, cameraPermanentlyDenied = cameraPermanentlyDenied ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 37e1882..000df80 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,6 @@ lifecycleRuntimeKtx = "2.8.7" activityCompose = "1.9.3" composeBom = "2024.12.01" camerax = "1.4.1" -accompanist = "0.36.0" exifinterface = "1.3.7" playServicesLocation = "21.3.0" @@ -36,9 +35,6 @@ androidx-exifinterface = { group = "androidx.exifinterface", name = "exifinterfa # Location play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" } -# Accompanist for permissions -accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } - [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/version.properties b/version.properties index a074999..dca2f22 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ versionMajor=1 versionMinor=1 -versionPatch=1 -versionCode=3 +versionPatch=2 +versionCode=4