summaryrefslogtreecommitdiff
path: root/android-app/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src')
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/ui/LocationPermissionHandler.kt43
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/ui/MainActivity.kt33
-rw-r--r--android-app/app/src/test/kotlin/com/example/androidapp/ui/LocationPermissionHandlerTest.kt110
3 files changed, 175 insertions, 11 deletions
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<String, Boolean>) {
+ 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() {
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/ui/LocationPermissionHandlerTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/ui/LocationPermissionHandlerTest.kt
new file mode 100644
index 0000000..54afc26
--- /dev/null
+++ b/android-app/app/src/test/kotlin/com/example/androidapp/ui/LocationPermissionHandlerTest.kt
@@ -0,0 +1,110 @@
+package com.example.androidapp.ui
+
+import org.junit.Assert.*
+import org.junit.Test
+
+class LocationPermissionHandlerTest {
+
+ // Convenience factory — callers override only the lambdas they care about.
+ private fun makeHandler(
+ checkGranted: () -> Boolean = { false },
+ onGranted: () -> Unit = {},
+ onDenied: () -> Unit = {},
+ requestPermissions: () -> Unit = {}
+ ) = LocationPermissionHandler(checkGranted, onGranted, onDenied, requestPermissions)
+
+ // ── start() ──────────────────────────────────────────────────────────────
+
+ @Test
+ fun `start - permission already granted - calls onGranted without requesting`() {
+ var onGrantedCalled = false
+ var requestCalled = false
+ makeHandler(
+ checkGranted = { true },
+ onGranted = { onGrantedCalled = true },
+ requestPermissions = { requestCalled = true }
+ ).start()
+
+ assertTrue("onGranted should be called", onGrantedCalled)
+ assertFalse("requestPermissions should NOT be called", requestCalled)
+ }
+
+ @Test
+ fun `start - permission not granted - calls requestPermissions without calling onGranted`() {
+ var onGrantedCalled = false
+ var requestCalled = false
+ makeHandler(
+ checkGranted = { false },
+ onGranted = { onGrantedCalled = true },
+ requestPermissions = { requestCalled = true }
+ ).start()
+
+ assertFalse("onGranted should NOT be called", onGrantedCalled)
+ assertTrue("requestPermissions should be called", requestCalled)
+ }
+
+ // ── onResult() ───────────────────────────────────────────────────────────
+
+ @Test
+ fun `onResult - fine location granted - calls onGranted`() {
+ var onGrantedCalled = false
+ makeHandler(onGranted = { onGrantedCalled = true }).onResult(
+ mapOf(
+ "android.permission.ACCESS_FINE_LOCATION" to true,
+ "android.permission.ACCESS_COARSE_LOCATION" to false
+ )
+ )
+ assertTrue("onGranted should be called when fine location is granted", onGrantedCalled)
+ }
+
+ @Test
+ fun `onResult - coarse location granted - calls onGranted`() {
+ var onGrantedCalled = false
+ makeHandler(onGranted = { onGrantedCalled = true }).onResult(
+ mapOf(
+ "android.permission.ACCESS_FINE_LOCATION" to false,
+ "android.permission.ACCESS_COARSE_LOCATION" to true
+ )
+ )
+ assertTrue("onGranted should be called when coarse location is granted", onGrantedCalled)
+ }
+
+ @Test
+ fun `onResult - both permissions granted - calls onGranted`() {
+ var onGrantedCalled = false
+ makeHandler(onGranted = { onGrantedCalled = true }).onResult(
+ mapOf(
+ "android.permission.ACCESS_FINE_LOCATION" to true,
+ "android.permission.ACCESS_COARSE_LOCATION" to true
+ )
+ )
+ assertTrue(onGrantedCalled)
+ }
+
+ @Test
+ fun `onResult - all permissions denied - calls onDenied not onGranted`() {
+ var onGrantedCalled = false
+ var onDeniedCalled = false
+ makeHandler(
+ onGranted = { onGrantedCalled = true },
+ onDenied = { onDeniedCalled = true }
+ ).onResult(
+ mapOf(
+ "android.permission.ACCESS_FINE_LOCATION" to false,
+ "android.permission.ACCESS_COARSE_LOCATION" to false
+ )
+ )
+ assertFalse("onGranted should NOT be called", onGrantedCalled)
+ assertTrue("onDenied should be called", onDeniedCalled)
+ }
+
+ @Test
+ fun `onResult - empty grants (never ask again scenario) - calls onDenied`() {
+ var onDeniedCalled = false
+ makeHandler(onDenied = { onDeniedCalled = true }).onResult(emptyMap())
+ assertTrue(
+ "onDenied should be called for empty grants (never-ask-again)",
+ onDeniedCalled
+ )
+ }
+}