androidkotlinandroid-14

Uploading Images from Android App to Backend with Missing EXIF Data


I'm working on an Android application where users can select multiple photos from their device's gallery and upload them to a backend server. To achieve this, I'm using PickMultipleVisualMedia() to get URIs of the selected images and then passing them to a worker for uploading.

Here's how I've implemented the photo picker and the worker:

Photo Picker Implementation:

private fun launchNewPhotoPicker(){
    newPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}

val newPicker = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(5)) { uris ->
    if (uris != null) {
        Log.d("PhotoPicker", "Selected URIs: ${uris}")
        runUploadImageWorker(uris)
    } else {
        Log.d("PhotoPicker", "No media selected")
    }
}

private fun runUploadImageWorker(uris: List<Uri>) {
    val strUris = uris.map { it.toString() }.toTypedArray()
    val data = Data.Builder()
        .putStringArray("uris", strUris)
        .build()

    val uploadImageRequest = OneTimeWorkRequestBuilder<UploadImageWorker>()
        .setInputData(data)
        .build()

    WorkManager.getInstance(requireContext()).enqueue(uploadImageRequest)
}

Worker Implementation:

class UploadImageWorker(appContext: Context, workerParams: WorkerParameters) :
    Worker(appContext, workerParams) {

    override fun doWork(): Result {
        val uris = inputData.getStringArray("uris") ?: return Result.failure()

        val client = okhttp3.OkHttpClient()

        for (uriString in uris) {
            val uri = Uri.parse(uriString)
            val inputStream = applicationContext.contentResolver.openInputStream(uri)
            val requestBody = inputStream?.readBytes()?.toRequestBody("image/*".toMediaTypeOrNull())
            val body = requestBody?.let {
                MultipartBody.Part.createFormData("picture", UUID.randomUUID().toString(), it)
            } ?: return Result.failure()

            val multipartBody = MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addPart(body)
                .build()

            val request = okhttp3.Request.Builder()
                .url("https://mywonderfullfakeurl.com/api/pictures")
                .post(multipartBody)
                .build()

            val response = client.newCall(request).execute()

            if (!response.isSuccessful) {
                val errorBody = response.body?.string() ?: "Unknown error"
                Log.d("error", errorBody)
                return Result.failure()
            }
        }

        return Result.success()
    }
}

However, I've noticed that the images uploaded to my backend server are missing EXIF data. How can I ensure that the EXIF data is preserved when uploading images from an Android app using Kotlin?


Solution

  • To preserve EXIF data when uploading images from an Android app using Kotlin, you can use the following technique, inspired by @Maveňツ, along with using a temporary file to avoid ENOENT errors. Additionally, as @blackapps pointed out, using OpenMultipleDocuments() instead of PickMultipleVisualMedia() is crucial to prevent null EXIF data when reading from the URI.

    Photo Picker Implementation:

        private fun launchNewPhotoPicker(){
            newPiker.launch(arrayOf("image/jpeg"))
        }
     
        private val newPiker=registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris ->
            if (uris != null) {
                Log.d("PhotoPicker", "Selected URI: ${uris}")
                //launch the worker to upload the images
                runUploadImageWorker(uris)
            } else {
                Log.d("PhotoPicker", "No media selected")
            }
        }
    

    Worker Implementation:

    override fun doWork(): Result {
        val uris = inputData.getStringArray("uris") ?: return Result.failure()
    
        val client = okhttp3.OkHttpClient()
    
        for (uriString in uris) {
            val uri = Uri.parse(uriString)
            // create temp file
            val uuid = UUID.randomUUID().toString()
            val file = File(applicationContext.cacheDir, uuid)
            // copy selected file to temp file
            uri?.let { applicationContext.contentResolver.openInputStream(it) }.use { input ->
                file.outputStream().use { output ->
                    input?.copyTo(output)
                }
            }
    
            val inputStream = file.inputStream()
            val requestBody = inputStream.readBytes().toRequestBody("image/*".toMediaTypeOrNull())
            val body = requestBody.let {
                MultipartBody.Part.createFormData("picture", UUID.randomUUID().toString(), it)
            }
    
            val multipartBodyBuilder = MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addPart(body)
    
            // Preserving EXIF data
            val exifInterface = ExifInterface(file)
            val tagsToCheck = arrayOf(
                ExifInterface.TAG_DATETIME,
                ExifInterface.TAG_GPS_LATITUDE,
                ExifInterface.TAG_GPS_LONGITUDE,
                ExifInterface.TAG_GPS_ALTITUDE
            )
    
            for (tag in tagsToCheck) {
                exifInterface.getAttribute(tag)?.let {
                    multipartBodyBuilder.addFormDataPart("exif_$tag", it)
                }
            }
    
            val multipartBody = multipartBodyBuilder.build()
            // don't forget to delete the temporary file
            file.delete()
    
            val request = okhttp3.Request.Builder()
                .url("https://mywonderfullfakeurl.com/api/pictures")
                .post(multipartBody)
                .build()
    
            val response = client.newCall(request).execute()
    
            if (!response.isSuccessful) {
                val errorBody = response.body?.string() ?: "Unknown error"
                Log.d("error", errorBody)
                return Result.failure()
            }
        }
    
        return Result.success()
    }
    

    In this revised version:

    This ensures that the uploaded images preserve their selected EXIF data when sent to the backend server.