Search code examples
androidkotlinretrofit2multipartform-datascoped-storage

Image Upload issue with retrofit for android 10 kotlin


Api Interface :

@Multipart
@POST(AppConstants.UPDATE_PRODUCT)
fun updateProduct(
    @Header("Authorization") auth: String,
    @Part("product_id") id: RequestBody,
    @Part("title") title: RequestBody,
    @Part("description") description: RequestBody,
    @Part("price") price: RequestBody,
    @Part image: MultipartBody.Part
) : Call<UpdateProductResponse>?

Intent :

val photoIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
                    type = "image/*"
                    addCategory(Intent.CATEGORY_OPENABLE)
                }
                startActivityForResult(photoIntent, UPLOAD_IMAGE)

Activity result :

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == UPLOAD_IMAGE && resultCode == RESULT_OK) {
        if (data != null) {
            val bitmap = MediaStore.Images.Media.getBitmap(this.contentResolver, data.data)
            update_iv.setImageBitmap(bitmap)

            val imageType = contentResolver.getType(data.data!!)
            val extension = imageType!!.substring(imageType.indexOf("/") + 1)

            val file = File(getPath(data.data!!, this)!!)
            
            //val byte = contentResolver.openInputStream(data.data!!)
            //val contentPart = InputStreamRequestBody("image/*".toMediaType() , contentResolver , data.data!!)

            //filePartImage = MultipartBody.Part.createFormData("image" , file.name , contentPart)
            data.data!!.let {
                application.contentResolver.openInputStream(it)?.use { inputStream ->
                    filePartImage = MultipartBody.Part.createFormData(
                        "image",
                        "" + file.name, //or "image.$extension" none of the two works
                        inputStream.readBytes().toRequestBody("image/*".toMediaType())
                    )
                }
            }
            showMessage(this, "" + getString(R.string.image_uploaded))
        } else {
            showMessage(this, "" + "File upload failed!")
        }
    }
}

Api call : Here filePartImage is global variable like this : private var filePartImage: MultipartBody.Part? = null

    RetrofitClientInstance().createService(ArtOFiestaServiceClass::class.java).updateProduct(
        "Bearer " + Prefs.getString("token", ""),
        id.trim().toRequestBody("text/plain".toMediaType()),
        update_name.text.toString().trim().toRequestBody("text/plain".toMediaType()),
        update_desc.text.toString().trim().toRequestBody("text/plain".toMediaType()),
        update_price.text.toString().trim().toRequestBody("text/plain".toMediaType()),
        filePartImage!!
    )?.enqueue(object : retrofit2.Callback<UpdateProductResponse> {
        override fun onResponse(
            call: Call<UpdateProductResponse>,
            response: Response<UpdateProductResponse>
        )

Manifest :

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
android:requestLegacyExternalStorage="true"

I have developed it's backend in node js and using multer to upload files. I can upload a file using postman but fail from my mobile application. What is wrong with my code ? Could it be android Scoped Storage issue ?


Solution

  • The problem is due to Scoped Storage, because I am also facing the same problem as well.

    My solution is convert inputStream to identical File and store it somewhere else. Then after upload, you may delete the file.

    The idea is as below.

    contentResolver.openInputStream(it)?.use { inputStream ->
        // STEP 1: Create a tempFile for storing the image from scoped storage. 
        val tempFile = createTempFile(this, fileName, extension)
    
        // STEP 2: copy inputStream into the tempFile
        copyStreamToFile(it, tempFile)
    
        // STEP 3: get file path from tempFile for further upload process. 
        val filePath = tempFile.absolutePath
    
        val requestFile: RequestBody = File(filePath).asRequestBody(fileType?.toMediaTypeOrNull())
        val body: MultipartBody.Part = MultipartBody.Part.createFormData("file", fileName, requestFile)
    }
    
    @Throws(IOException::class)
    fun createTempFile(context: Context, fileName: String?, extension: String?): File {
        val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
        return File(storageDir, "$fileName.$extension")
    }
    
    fun copyStreamToFile(inputStream: InputStream, outputFile: File) {
        inputStream.use { input ->
            val outputStream = FileOutputStream(outputFile)
            outputStream.use { output ->
                val buffer = ByteArray(4 * 1024) // buffer size
                while (true) {
                    val byteCount = input.read(buffer)
                    if (byteCount < 0) break
                    output.write(buffer, 0, byteCount)
                }
                output.flush()
            }
        }
     }