diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt index 256e17451d..2b4586ecce 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt @@ -15,7 +15,11 @@ */ package com.google.android.ground.ui.datacollection.tasks.photo +import android.content.Context import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Matrix +import android.media.ExifInterface import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData @@ -25,7 +29,9 @@ import com.google.android.ground.persistence.remote.firebase.FirebaseStorageMana import com.google.android.ground.repository.UserMediaRepository import com.google.android.ground.ui.datacollection.tasks.AbstractTaskViewModel import com.google.android.ground.ui.util.BitmapUtil +import dagger.hilt.android.qualifiers.ApplicationContext import java.io.IOException +import java.lang.UnsupportedOperationException import javax.inject.Inject import kotlinx.coroutines.flow.map import timber.log.Timber @@ -33,6 +39,7 @@ import timber.log.Timber class PhotoTaskViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val userMediaRepository: UserMediaRepository, private val bitmapUtil: BitmapUtil, resources: Resources, @@ -50,6 +57,15 @@ constructor( taskTaskData.map { userMediaRepository.getDownloadUrl(it?.getDetailsText()) }.asLiveData() val isPhotoPresent: LiveData = taskTaskData.map { it.isNotNullOrEmpty() }.asLiveData() + private fun rotateBitmap(bitmap: Bitmap, rotateDegrees: Float): Bitmap { + val matrix = Matrix() + // Rotate iff rotation is non-zero. + if (rotateDegrees != 0f) { + matrix.postRotate(rotateDegrees) + } + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true) + } + /** * Saves photo data stored on an on-device URI in Ground-associated storage and prepares it for * inclusion in a data collection submission. @@ -59,7 +75,9 @@ constructor( requireNotNull(currentTask) { "Photo captured but no task waiting for the result" } try { - val bitmap = bitmapUtil.fromUri(uri) + val orientation = getOrientationFromExif(uri) + val rotateDegrees = getRotationDegrees(orientation) + val bitmap = rotateBitmap(bitmapUtil.fromUri(uri), rotateDegrees) val file = userMediaRepository.savePhoto(bitmap, currentTask) userMediaRepository.addImageToGallery(file.absolutePath, file.name) val remoteFilename = FirebaseStorageManager.getRemoteMediaPath(surveyId, file.absolutePath) @@ -68,4 +86,26 @@ constructor( Timber.e(e, "Error getting photo selected from storage") } } + + /** + * Returns the number of degrees a photo should be rotated based on the value of its orientation + * EXIF tag. + */ + private fun getRotationDegrees(orientation: Int): Float = + when (orientation) { + ExifInterface.ORIENTATION_NORMAL -> 0f + ExifInterface.ORIENTATION_ROTATE_90 -> 90f + ExifInterface.ORIENTATION_ROTATE_180 -> 180f + ExifInterface.ORIENTATION_ROTATE_270 -> 270f + else -> throw UnsupportedOperationException("Unsupported photo orientation $orientation") + } + + /** Returns the EXIF orientation attribute of the JPEG image at the specified URI. */ + private fun getOrientationFromExif(uri: Uri): Int { + val inputStream = + context.contentResolver.openInputStream(uri) + ?: throw IOException("Content resolver returned null for $uri") + val exif = ExifInterface(inputStream) + return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + } }