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..c2c07af --- /dev/null +++ b/app/src/main/java/com/example/snapandsolve/camera/AlbumViewModel.kt @@ -0,0 +1,117 @@ +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 -> { + // Create an empty image file in the app's cache directory + val tempFile = File.createTempFile( + "temp_image_file_", /* prefix */ + ".jpg", /* suffix */ + intent.compositionContext.cacheDir /* cache directory */ + ) + + // Create sandboxed url for this temp file - needed for the camera API + val uri = FileProvider.getUriForFile(intent.compositionContext, + "${BuildConfig.APPLICATION_ID}.provider", /* needs to match the provider information in the manifest */ + tempFile + ) + _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.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