diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..7643783
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..4bba6b0 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bab9889..07cd712 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -12,12 +12,19 @@ android {
defaultConfig {
applicationId = "com.example.snapandsolve"
- minSdk = 27
+ minSdk = 28
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ val properties = org.jetbrains.kotlin.konan.properties.Properties()
+ val propertiesFile = rootProject.file("local.properties")
+ if (propertiesFile.exists()){
+ propertiesFile.inputStream().use { properties.load(it) }
+ }
+
+ buildConfigField("String", "ARCGIS_TOKEN","\"${properties.getProperty("arcgis.token","")}\"")
}
buildTypes {
@@ -38,6 +45,7 @@ android {
}
buildFeatures {
compose = true
+ buildConfig = true
}
}
@@ -59,4 +67,11 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.test.manifest)
implementation("androidx.compose.material:material-icons-core")
implementation("androidx.compose.material:material-icons-extended")
+
+ // ArcGIS Maps for Kotlin - SDK dependency
+ implementation(libs.arcgis.maps.kotlin)
+ // Toolkit dependencies
+ implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom))
+ implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose)
+ implementation(libs.arcgis.maps.kotlin.toolkit.authentication)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1936271..1e9787f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,12 +1,14 @@
-
+
+
+
+
-
-
+
-
+
@@ -28,6 +30,15 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/snapandsolve/MainActivity.kt b/app/src/main/java/com/example/snapandsolve/MainActivity.kt
index 233be42..c10aa31 100644
--- a/app/src/main/java/com/example/snapandsolve/MainActivity.kt
+++ b/app/src/main/java/com/example/snapandsolve/MainActivity.kt
@@ -7,15 +7,27 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
+import com.arcgismaps.ApiKey
+import com.arcgismaps.ArcGISEnvironment
+import com.example.snapandsolve.camera.AlbumViewModel
import com.example.snapandsolve.ui.theme.SnapAndSolveTheme
+import kotlinx.coroutines.Dispatchers
class MainActivity : ComponentActivity() {
+ private lateinit var viewModel: AlbumViewModel
+
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
+ viewModel = AlbumViewModel(coroutineContext = Dispatchers.Default)
+
+ /*
+ Wird gebraucht um die Karte in ArcGIS anzuzeigen. Die Prüfung ob man Zugang hat oder nicht
+ wurde gelöscht.
+ */
+ ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ARCGIS_TOKEN)
enableEdgeToEdge()
setContent {
diff --git a/app/src/main/java/com/example/snapandsolve/MainScreen.kt b/app/src/main/java/com/example/snapandsolve/MainScreen.kt
index 9e37781..d999c9e 100644
--- a/app/src/main/java/com/example/snapandsolve/MainScreen.kt
+++ b/app/src/main/java/com/example/snapandsolve/MainScreen.kt
@@ -1,6 +1,11 @@
package com.example.snapandsolve
+import android.Manifest
import android.app.Application
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -13,35 +18,34 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
-import androidx.compose.material3.FloatingActionButton
-import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeFloatingActionButton
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -50,10 +54,21 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
+import com.arcgismaps.mapping.ArcGISMap
+import com.arcgismaps.mapping.BasemapStyle
+import com.arcgismaps.mapping.Viewpoint
+import com.arcgismaps.toolkit.geoviewcompose.MapView
+import com.example.snapandsolve.camera.AlbumViewModel
+import com.example.snapandsolve.camera.AlbumViewState
+import com.example.snapandsolve.camera.Intent
import com.example.snapandsolve.ui.theme.AppColor
import com.example.snapandsolve.ui.theme.ButtonColor
import com.example.snapandsolve.ui.theme.WidgetColor
+import com.example.snapandsolve.ui.theme.setupLocationDisplay
+import kotlinx.coroutines.Dispatchers
@Composable
@@ -69,7 +84,6 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
BottomAppBar(
modifier = Modifier.height(120.dp),
containerColor = AppColor,
- // contentColor = AppColor
) {
Row(
modifier = Modifier
@@ -81,12 +95,11 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
onClick = {
/*TODO*/
},
- modifier = Modifier.padding(bottom = 8.dp) // Abstand "in" der Bar
+ modifier = Modifier.padding(bottom = 8.dp)
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu",
-
)
}
Spacer(Modifier.weight(1f))
@@ -105,8 +118,7 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
}
},
floatingActionButtonPosition = FabPosition.Center,
- ) {
- innerPadding ->
+ ) { innerPadding ->
ContentScreen(
modifier = Modifier.padding(innerPadding),
application,
@@ -123,13 +135,30 @@ fun ContentScreen(
onDismissReport: () -> Unit
) {
val mapViewModel = remember { MapViewModel(application) }
- Box(modifier = modifier.fillMaxSize()) {
+ val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) }
- // 2) Overlay
+ // ArcGIS Map erstellen
+ val map = remember {
+ createMap() //Funktion zur Erstellung der Map
+ }
+
+ //Standortbestimmung aus locationHelper.kt
+ val locationDisplay = setupLocationDisplay()
+
+ Box(modifier = modifier.fillMaxSize()) {
+ // HINTERGRUND: Die Map
+ MapView(
+ modifier = Modifier.fillMaxSize(),
+ arcGISMap = map,
+ locationDisplay = locationDisplay
+ )
+
+ // VORDERGRUND: Das Overlay (wenn showReport = true)
if (showReport) {
ReportOverlay(
onCancel = onDismissReport,
- onAdd = { /* später */ }
+ onAdd = { /* später */ },
+ viewModel = albumViewModel
)
}
}
@@ -148,15 +177,71 @@ fun AppTopBar(
containerColor = AppColor,
titleContentColor = Color.White
)
-
)
}
@Composable
fun ReportOverlay(
onCancel: () -> Unit,
- onAdd: () -> Unit
+ onAdd: () -> Unit,
+ viewModel: AlbumViewModel
) {
+ val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
+ val currentContext = LocalContext.current
+
+ // Launcher für Bildauswahl aus Galerie
+ val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.PickMultipleVisualMedia(20)
+ ) { urls ->
+ viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
+ }
+
+ // Launcher für Kamera
+ val cameraLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.TakePicture()
+ ) { isImageSaved ->
+ if (isImageSaved) {
+ viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
+ } else {
+ viewModel.onReceive(Intent.OnImageSavingCanceled)
+ }
+ }
+
+ // Launcher für Kamera-Berechtigung
+ val permissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { permissionGranted ->
+ if (permissionGranted) {
+ viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
+ } else {
+ viewModel.onReceive(Intent.OnPermissionDenied)
+ }
+ }
+
+ // Funktion zum Starten der Kamera (prüft Berechtigung)
+ fun startCamera() {
+ println("DEBUG: startCamera() aufgerufen")
+ val hasPermission = currentContext.checkSelfPermission(Manifest.permission.CAMERA) ==
+ android.content.pm.PackageManager.PERMISSION_GRANTED
+ println("DEBUG: Hat Berechtigung? $hasPermission")
+ if (hasPermission) {
+ println("DEBUG: Erstelle tempFileUrl")
+ // Berechtigung bereits erteilt -> direkt tempFileUrl erstellen
+ viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
+ } else {
+ println("DEBUG: Frage Berechtigung an")
+ // Berechtigung anfragen
+ permissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+ }
+
+ // Kamera starten, wenn tempFileUrl gesetzt ist
+ LaunchedEffect(key1 = viewState.tempFileUrl) {
+ viewState.tempFileUrl?.let {
+ cameraLauncher.launch(it)
+ }
+ }
+
// leichter Dim-Hintergrund
Box(
modifier = Modifier
@@ -179,14 +264,41 @@ fun ReportOverlay(
Column(
modifier = Modifier
.fillMaxWidth()
- .padding(20.dp),
+ .padding(20.dp)
+ .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
- Text("Schadensbeschreibung:",
+ Text(
+ "Schadensbeschreibung:",
color = Color.Black
)
- // Platzhalter fürs Textfeld / Icons etc.
+ // Kamera-Buttons (über der weißen Box)
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Button(
+ onClick = {
+ startCamera()
+ },
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(text = "Foto aufnehmen")
+ }
+ Button(
+ onClick = {
+ pickImageFromAlbumLauncher.launch(
+ PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
+ )
+ },
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(text = "Aus Galerie")
+ }
+ }
+
+ // Die weiße Textfeld Box
Box(
modifier = Modifier
.fillMaxWidth()
@@ -194,6 +306,30 @@ fun ReportOverlay(
.background(Color.White, RoundedCornerShape(12.dp))
)
+ // Grid für ausgewählte Bilder (außerhalb der Box, aber in der Card)
+ if (viewState.selectedPictures.isNotEmpty()) {
+ Text(
+ text = "Ausgewählte Bilder (${viewState.selectedPictures.size})",
+ color = Color.Black
+ )
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(3),
+ userScrollEnabled = false,
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(0.dp, 200.dp)
+ ) {
+ itemsIndexed(viewState.selectedPictures) { index, picture ->
+ Image(
+ modifier = Modifier.padding(4.dp),
+ bitmap = picture,
+ contentDescription = "Bild ${index + 1}",
+ contentScale = ContentScale.Crop
+ )
+ }
+ }
+ }
+
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
@@ -206,16 +342,32 @@ fun ReportOverlay(
disabledContainerColor = Color.White,
disabledContentColor = Color.White
)
- ) { Text(
- "Abbrechen",
- color = Color.Black
- ) }
+ ) {
+ Text(
+ "Abbrechen",
+ color = Color.Black
+ )
+ }
Button(onClick = onAdd) { Text("Hinzufügen") }
}
}
}
}
+
+
}
+fun createMap(): ArcGISMap {
+
+ return ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
+
+ initialViewpoint = Viewpoint(
+ 53.14,
+ 8.20,
+ 20000.0)
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/snapandsolve/MapViewModel.kt b/app/src/main/java/com/example/snapandsolve/MapViewModel.kt
index 15aa574..772f85d 100644
--- a/app/src/main/java/com/example/snapandsolve/MapViewModel.kt
+++ b/app/src/main/java/com/example/snapandsolve/MapViewModel.kt
@@ -1,8 +1,64 @@
package com.example.snapandsolve
import android.app.Application
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.arcgismaps.data.ArcGISFeature
+import com.arcgismaps.data.CodedValueDomain
+import com.arcgismaps.data.ServiceFeatureTable
+import com.arcgismaps.mapping.ArcGISMap
+import com.arcgismaps.mapping.BasemapStyle
+import com.arcgismaps.mapping.Viewpoint
+import com.arcgismaps.mapping.layers.FeatureLayer
+import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
+import kotlinx.coroutines.launch
class MapViewModel(application: Application ): AndroidViewModel(application) {
+ val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply {
+ initialViewpoint = Viewpoint(53.14, 8.20, 20000.0)
+ }
+
+ /*
+ ALLES UNTER DIESEM KOMMENTAR WIRD NICHT GENUTZT. Aber EVENTUELL nötig zum einbinden von
+ Layer und Features.
+ */
+ // Hold a reference to the selected feature.
+ var selectedFeature: ArcGISFeature? by mutableStateOf(null)
+ val mapViewProxy = MapViewProxy()
+
+ //var currentFeatureOperation by mutableStateOf(FeatureOperationType.CREATE)
+
+ lateinit var featureLayer: FeatureLayer
+
+ // Create a snackbar message to display the result of feature operations.
+ var snackBarMessage: String by mutableStateOf("")
+
+ lateinit var serviceFeatureTable: ServiceFeatureTable
+
+ var currentDamageType by mutableStateOf("")
+
+ // The list of damage types to update the feature attribute.
+ var damageTypeList: List = mutableListOf()
+
+ init {
+ viewModelScope.launch {
+ serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/ArcGIS/rest/services/StrassenSchaeden/FeatureServer/0")
+ serviceFeatureTable.load().onSuccess {
+ // Get the field from the feature table that will be updated.
+ val typeDamageField = serviceFeatureTable.fields.first { it.name == "Typ" }
+ // Get the coded value domain for the field.
+ val attributeDomain = typeDamageField.domain as CodedValueDomain
+ // Add the damage types to the list.
+ attributeDomain.codedValues.forEach {
+ damageTypeList += it.name
+ }
+ }
+ featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable)
+ map.operationalLayers.add(featureLayer)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/snapandsolve/camera/AlbumAndroidViewModel.kt b/app/src/main/java/com/example/snapandsolve/camera/AlbumAndroidViewModel.kt
new file mode 100644
index 0000000..541cb74
--- /dev/null
+++ b/app/src/main/java/com/example/snapandsolve/camera/AlbumAndroidViewModel.kt
@@ -0,0 +1,119 @@
+package com.example.snapandsolve.camera
+
+
+import android.app.Application
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.ImageDecoder
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.core.content.FileProvider
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.snapandsolve.BuildConfig
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import java.io.File
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * This variant inherits from [AndroidViewModel] and has access to the application context
+ */
+class AlbumAndroidViewModel(private val appContext: Application,
+ private val coroutineContext: CoroutineContext
+): AndroidViewModel(appContext) {
+ //region View State
+ private val _albumViewState: MutableStateFlow = MutableStateFlow(
+ AlbumViewState()
+ )
+ val viewStateFlow: StateFlow
+ get() = _albumViewState
+ //endregion
+
+ fun onEvent(intent: Intent) = viewModelScope.launch(coroutineContext) {
+ when(intent) {
+ is Intent.OnPermissionGranted -> {
+ // Create an empty image file in the app's cache directory
+ val file = File.createTempFile(
+ "temp_image_file_", /* prefix */
+ ".jpg", /* suffix */
+ appContext.cacheDir /* cache directory */
+ )
+
+ // Create sandboxed url for this temp file - needed for the camera API
+ val uri = FileProvider.getUriForFile(appContext,
+ "${BuildConfig.APPLICATION_ID}.provider",
+ file
+ )
+ _albumViewState.value = _albumViewState.value.copy(tempFileUrl = uri)
+ }
+
+ is Intent.OnPermissionDenied -> {
+ // maybe log the permission denial event
+ println("User did not grant permission to use the camera")
+ }
+
+ is Intent.OnFinishPickingImages -> {
+ if (intent.imageUrls.isNotEmpty()) {
+ // Handle picked images
+ val newImages = mutableListOf()
+ for (eachImageUrl in intent.imageUrls) {
+ val inputStream = appContext.contentResolver.openInputStream(eachImageUrl)
+ val bytes = inputStream?.readBytes()
+ inputStream?.close()
+
+ if (bytes != null) {
+ val bitmapOptions = BitmapFactory.Options()
+ bitmapOptions.inMutable = true
+ val bitmap: Bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bitmapOptions)
+ val imageBitmap = bitmap.asImageBitmap()
+ newImages.add(imageBitmap)
+ } else {
+ // error reading the bytes from the image url
+ println("The image that was picked could not be read from the device at this url: $eachImageUrl")
+ }
+ }
+
+ val currentViewState = _albumViewState.value
+ val newCopy = currentViewState.copy(
+ selectedPictures = (currentViewState.selectedPictures + newImages),
+ tempFileUrl = null
+ )
+ _albumViewState.value = newCopy
+ } else {
+ // user did not pick anything
+ }
+ }
+
+ is Intent.OnImageSaved -> {
+ val tempImageUrl = _albumViewState.value.tempFileUrl
+ if (tempImageUrl != null) {
+ val source: ImageDecoder.Source = ImageDecoder.createSource(appContext.contentResolver, tempImageUrl)
+
+ val currentPictures = _albumViewState.value.selectedPictures.toMutableList()
+ currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap())
+
+ _albumViewState.value = _albumViewState.value.copy(tempFileUrl = null,
+ selectedPictures = currentPictures)
+ }
+ }
+
+ is Intent.OnImageSavingCanceled -> {
+ _albumViewState.value = _albumViewState.value.copy(tempFileUrl = null)
+ }
+
+ is Intent.OnFinishPickingImagesWith -> {
+ // unnecessary in this viewmodel variant
+ }
+
+ is Intent.OnPermissionGrantedWith -> {
+ // unnecessary in this viewmodel variant
+ }
+
+ is Intent.OnImageSavedWith -> {
+ // unnecessary in this viewmodel variant
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/snapandsolve/camera/AlbumEvents.kt b/app/src/main/java/com/example/snapandsolve/camera/AlbumEvents.kt
new file mode 100644
index 0000000..49c4d6b
--- /dev/null
+++ b/app/src/main/java/com/example/snapandsolve/camera/AlbumEvents.kt
@@ -0,0 +1,28 @@
+package com.example.snapandsolve.camera
+
+import android.content.Context
+import android.net.Uri
+/*
+Das sind die Funktionen beim Drücken der Knöpfe. Übersichtlicher wäre es sie direkt mit den
+Knöpfen in der ViewModel zu platzieren. AlbumEvents als name ist vielleicht unglücklich gewählt.
+ */
+/**
+ * User generated events that can be triggered from the UI.
+ */
+sealed class Intent {
+ data object OnPermissionGranted: Intent()
+
+ data class OnPermissionGrantedWith(val compositionContext: Context): Intent()
+
+ data object OnPermissionDenied: Intent()
+
+ data object OnImageSaved: Intent()
+
+ data class OnImageSavedWith (val compositionContext: Context): Intent()
+
+ data object OnImageSavingCanceled: Intent()
+
+ data class OnFinishPickingImages(val imageUrls: List): Intent()
+
+ data class OnFinishPickingImagesWith(val compositionContext: Context, val imageUrls: List): Intent()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/snapandsolve/camera/AlbumViewModel.kt b/app/src/main/java/com/example/snapandsolve/camera/AlbumViewModel.kt
new file mode 100644
index 0000000..10d780a
--- /dev/null
+++ b/app/src/main/java/com/example/snapandsolve/camera/AlbumViewModel.kt
@@ -0,0 +1,119 @@
+package com.example.snapandsolve.camera
+
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.ImageDecoder
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.core.content.FileProvider
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.snapandsolve.BuildConfig
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import java.io.File
+import kotlin.coroutines.CoroutineContext
+
+class AlbumViewModel(private val coroutineContext: CoroutineContext
+): ViewModel() {
+
+ //region View State
+ private val _albumViewState: MutableStateFlow = MutableStateFlow(
+ AlbumViewState()
+ )
+ val viewStateFlow: StateFlow
+ get() = _albumViewState
+ //endregion
+
+ // region Intents
+ fun onReceive(intent: Intent) = viewModelScope.launch(coroutineContext) {
+ when(intent) {
+ is Intent.OnPermissionGrantedWith -> {
+ println("DEBUG: OnPermissionGrantedWith empfangen")
+ val tempFile = File.createTempFile(
+ "temp_image_file_",
+ ".jpg",
+ intent.compositionContext.cacheDir
+ )
+ println("DEBUG: TempFile erstellt: ${tempFile.absolutePath}")
+
+ val uri = FileProvider.getUriForFile(intent.compositionContext,
+ "${BuildConfig.APPLICATION_ID}.provider",
+ tempFile
+ )
+ println("DEBUG: URI erstellt: $uri")
+ _albumViewState.value = _albumViewState.value.copy(tempFileUrl = uri)
+ println("DEBUG: tempFileUrl gesetzt in ViewState")
+ }
+
+ is Intent.OnPermissionDenied -> {
+ // maybe log the permission denial event
+ println("User did not grant permission to use the camera")
+ }
+
+ is Intent.OnFinishPickingImagesWith -> {
+ if (intent.imageUrls.isNotEmpty()) {
+ // Handle picked images
+ val newImages = mutableListOf()
+ for (eachImageUrl in intent.imageUrls) {
+ val inputStream = intent.compositionContext.contentResolver.openInputStream(eachImageUrl)
+ val bytes = inputStream?.readBytes()
+ inputStream?.close()
+
+ if (bytes != null) {
+ val bitmapOptions = BitmapFactory.Options()
+ bitmapOptions.inMutable = true
+ val bitmap: Bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bitmapOptions)
+ newImages.add(bitmap.asImageBitmap())
+ } else {
+ // error reading the bytes from the image url
+ println("The image that was picked could not be read from the device at this url: $eachImageUrl")
+ }
+ }
+
+ val currentViewState = _albumViewState.value
+ val newCopy = currentViewState.copy(
+ selectedPictures = (currentViewState.selectedPictures + newImages),
+ tempFileUrl = null
+ )
+ _albumViewState.value = newCopy
+ } else {
+ // user did not pick anything
+ }
+ }
+
+ is Intent.OnImageSavedWith -> {
+ val tempImageUrl = _albumViewState.value.tempFileUrl
+ if (tempImageUrl != null) {
+ val source = ImageDecoder.createSource(intent.compositionContext.contentResolver, tempImageUrl)
+
+ val currentPictures = _albumViewState.value.selectedPictures.toMutableList()
+ currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap())
+
+ _albumViewState.value = _albumViewState.value.copy(tempFileUrl = null,
+ selectedPictures = currentPictures)
+ }
+ }
+
+ is Intent.OnImageSavingCanceled -> {
+ _albumViewState.value = _albumViewState.value.copy(tempFileUrl = null)
+ }
+
+ is Intent.OnPermissionGranted -> {
+ // unnecessary in this viewmodel variant
+ }
+
+ is Intent.OnFinishPickingImages -> {
+ // unnecessary in this viewmodel variant
+ }
+
+ is Intent.OnImageSaved -> {
+ // unnecessary in this viewmodel variant
+ }
+ }
+ }
+ // endregion
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/snapandsolve/camera/AlbumViewState.kt b/app/src/main/java/com/example/snapandsolve/camera/AlbumViewState.kt
new file mode 100644
index 0000000..3c7dd70
--- /dev/null
+++ b/app/src/main/java/com/example/snapandsolve/camera/AlbumViewState.kt
@@ -0,0 +1,19 @@
+package com.example.snapandsolve.camera
+
+import android.net.Uri
+import androidx.compose.ui.graphics.ImageBitmap
+
+/**
+ * Holds state data for the MainScreen composable.
+ */
+data class AlbumViewState(
+ /**
+ * holds the URL of the temporary file which stores the image taken by the camera.
+ */
+ val tempFileUrl: Uri? = null,
+
+ /**
+ * holds the list of images taken by camera or selected pictures from the gallery.
+ */
+ val selectedPictures: List = emptyList(),
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/locationHelper.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/locationHelper.kt
new file mode 100644
index 0000000..e903832
--- /dev/null
+++ b/app/src/main/java/com/example/snapandsolve/ui/theme/locationHelper.kt
@@ -0,0 +1,109 @@
+package com.example.snapandsolve.ui.theme
+
+
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.core.content.ContextCompat
+import com.arcgismaps.location.LocationDisplayAutoPanMode
+import com.arcgismaps.mapping.view.LocationDisplay
+import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
+import kotlinx.coroutines.launch
+
+/**
+ * Helper-Klasse für Standort-Funktionalität
+ */
+class LocationHelper(private val context: Context) {
+
+ /**
+ * Prüft, ob Standort-Berechtigungen erteilt wurden
+ */
+ fun hasLocationPermissions(): Boolean {
+ val coarseLocation = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+
+ val fineLocation = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+
+ return coarseLocation && fineLocation
+ }
+}
+
+/**
+ * Composable zum Einrichten des Location Display
+ *
+ * @param autoPanMode Wie die Karte dem Standort folgen soll (default: Recenter)
+ * @return LocationDisplay-Objekt, das an MapView übergeben werden kann
+ */
+@Composable
+fun setupLocationDisplay(
+ autoPanMode: LocationDisplayAutoPanMode = LocationDisplayAutoPanMode.Recenter
+): LocationDisplay {
+ val context = androidx.compose.ui.platform.LocalContext.current
+ val coroutineScope = rememberCoroutineScope()
+ val locationHelper = LocationHelper(context)
+
+ val locationDisplay = rememberLocationDisplay().apply {
+ setAutoPanMode(autoPanMode)
+ }
+
+ if (locationHelper.hasLocationPermissions()) {
+ LaunchedEffect(Unit) {
+ locationDisplay.dataSource.start()
+ }
+ } else {
+ RequestLocationPermissions(
+ context = context,
+ onPermissionsGranted = {
+ coroutineScope.launch {
+ locationDisplay.dataSource.start()
+ }
+ }
+ )
+ }
+
+ return locationDisplay
+}
+
+/**
+ * Composable zum Anfordern von Standort-Berechtigungen
+ */
+@Composable
+private fun RequestLocationPermissions(
+ context: Context,
+ onPermissionsGranted: () -> Unit
+) {
+ val activityResultLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ if (permissions.all { it.value }) {
+ onPermissionsGranted()
+ } else {
+ Toast.makeText(
+ context,
+ "Standort-Berechtigung wurde verweigert",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ activityResultLauncher.launch(
+ arrayOf(
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..4bf3065
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a4384c4..b72ef65 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,15 +1,15 @@
[versions]
-agp = "8.13.0"
+agp = "8.13.2"
kotlin = "2.0.21"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
-lifecycleRuntimeKtx = "2.9.4"
-activityCompose = "1.11.0"
-composeBom = "2025.12.00"
-arcgisMapsKotlin = "200.8.0"
+lifecycleRuntimeKtx = "2.10.0"
+activityCompose = "1.12.1"
+composeBom = "2024.09.00"
material3 = "1.4.0"
+arcgisMapsKotlin = "200.8.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -25,17 +25,15 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
arcgis-maps-kotlin = { group = "com.esri", name = "arcgis-maps-kotlin", version.ref = "arcgisMapsKotlin" }
arcgis-maps-kotlin-toolkit-bom = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-bom", version.ref = "arcgisMapsKotlin" }
arcgis-maps-kotlin-toolkit-geoview-compose = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-geoview-compose" }
arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" }
-androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
-
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
-
-
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a47d2e8..707bd12 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -16,6 +16,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven { url = uri("https://esri.jfrog.io/artifactory/arcgis") } // <-- NEU
}
}