From 0923c55af5c63539055933509302233ee3f4b26a Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Sat, 14 Mar 2026 00:50:17 +0000 Subject: feat: add LocationPermissionHandler with 7 unit tests for permission flows Extract location permission decision logic from MainActivity into a testable LocationPermissionHandler class. Covers: permission already granted, needs request, fine-only granted, coarse-only granted, both granted, both denied, and never-ask-again (empty grants) scenarios. All permissions (INTERNET, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION) were already declared in AndroidManifest.xml; no manifest changes needed. Co-Authored-By: Claude Sonnet 4.6 --- .../androidapp/ui/LocationPermissionHandler.kt | 43 ++++++++++++++++++++++ .../com/example/androidapp/ui/MainActivity.kt | 33 +++++++++++------ 2 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 android-app/app/src/main/kotlin/com/example/androidapp/ui/LocationPermissionHandler.kt (limited to 'android-app/app/src/main') diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/LocationPermissionHandler.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/LocationPermissionHandler.kt new file mode 100644 index 0000000..664d5bb --- /dev/null +++ b/android-app/app/src/main/kotlin/com/example/androidapp/ui/LocationPermissionHandler.kt @@ -0,0 +1,43 @@ +package com.example.androidapp.ui + +/** + * Encapsulates location permission decision logic. + * + * Extracted for testability — no direct Android framework dependency in the core logic. + * + * Permissions handled: ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION + * + * Usage: + * - Call [start] on activity start to check existing permission or trigger a request. + * - Call [onResult] from the ActivityResultLauncher callback with the permission grants map. + */ +class LocationPermissionHandler( + /** Returns true if location permission is already granted. */ + private val checkGranted: () -> Boolean, + /** Called when location permission is available (already granted or just granted). */ + private val onGranted: () -> Unit, + /** Called when location permission is denied or the user refuses (including "never ask again"). */ + private val onDenied: () -> Unit, + /** Called when permission needs to be requested from the user via the system dialog. */ + private val requestPermissions: () -> Unit +) { + /** + * Check current permission state and dispatch: + * - If already granted, invoke [onGranted] immediately. + * - Otherwise, invoke [requestPermissions] to trigger the system dialog. + */ + fun start() { + if (checkGranted()) onGranted() else requestPermissions() + } + + /** + * Process the result from the system permission dialog. + * + * @param grants Map of permission name → granted status from ActivityResultLauncher. + * Invokes [onGranted] if any permission was granted, [onDenied] otherwise. + * An empty map (e.g. "never ask again" scenario) also triggers [onDenied]. + */ + fun onResult(grants: Map) { + if (grants.values.any { it }) onGranted() else onDenied() + } +} diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainActivity.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainActivity.kt index 0a326f4..17a636f 100644 --- a/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainActivity.kt +++ b/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainActivity.kt @@ -24,11 +24,30 @@ class MainActivity : AppCompatActivity() { private val defaultLat = 37.8 private val defaultLon = -122.4 + private val permissionHandler: LocationPermissionHandler by lazy { + LocationPermissionHandler( + checkGranted = { + ContextCompat.checkSelfPermission( + this, Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + }, + onGranted = { fetchLocationAndLoad() }, + onDenied = { loadWeatherAtDefault() }, + requestPermissions = { + locationPermissionLauncher.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) + } + ) + } + private val locationPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { grants -> - val granted = grants.values.any { it } - if (granted) fetchLocationAndLoad() else loadWeatherAtDefault() + permissionHandler.onResult(grants) } override fun onCreate(savedInstanceState: Bundle?) { @@ -72,15 +91,7 @@ class MainActivity : AppCompatActivity() { } private fun requestLocationOrLoad() { - val fine = Manifest.permission.ACCESS_FINE_LOCATION - val coarse = Manifest.permission.ACCESS_COARSE_LOCATION - val hasPermission = ContextCompat.checkSelfPermission(this, fine) == - PackageManager.PERMISSION_GRANTED - if (hasPermission) { - fetchLocationAndLoad() - } else { - locationPermissionLauncher.launch(arrayOf(fine, coarse)) - } + permissionHandler.start() } private fun fetchLocationAndLoad() { -- cgit v1.2.3