How to prevent Ktor Client from encoding url parameters? - android

I am trying to create an android app with kotlin, this app need to have a mini download manager as I will need to download files from 100MB to 8GB and user can pause and resume download later when the server supports the pause, searching I found the Ktor library and reading the documentation plus some videos on youtube, I managed to write a base code where I could download the files and make the process of stopping the download and keep going all right when one of mine tests gave error there are files whose url pattern is: http://server.com/files?file=/10/55/file.zip
The problem is that I put this link, but Ktor converts to http://server.com/files?file=%2F10%2F55%2Ffile.zip this generate an error response on the server, as I don't have access to the server to change this rule I need to send the right url without encoding. Does anyone know how to do this? Prevent Ktor from doing a URL_encode in the url parameters, I couldn't find anything in the documentation
My code is this:
ktor-client version 1.6.7
fun startDownload(url: String, auth: String = "", userAgentS: String = "", fileName: String = ""){
val client = HttpClient(CIO)
val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File.createTempFile("File", "index", path)
runBlocking {
client.get<HttpStatement>(url){
headers {
append(HttpHeaders.Authorization, auth)
append(HttpHeaders.UserAgent, userAgentS)
append(HttpHeaders.Range, "bytes=${file.length()}-")
}
}
.execute { httpResponse ->
val channel: ByteReadChannel = httpResponse.receive()
while (!channel.isClosedForRead) {
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
while (!packet.isEmpty) {
val bytes = packet.readBytes()
file.appendBytes(bytes)
println("Received ${(file.length())} bytes from ${httpResponse.contentLength()}")
}
}
val pathF = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS + "/${fileName}")
file.renameTo(pathF)
println("A file saved to ${file.path}")
}
}
}
Can anyone help me solve this problem with ktor, if there is no solution, can someone tell me another way to achieve the same goal? Need to be with Kotlin.
update 2022-02-17
Thanks to Aleksei Tirman's help I managed to solve the problem, thank you very much. And the base code looks like this:
fun startDownload(url: String, auth: String = "", userAgentS: String = "", fileName: String = ""){
val client = HttpClient(CIO)
val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File.createTempFile("File", "index", path)
runBlocking {
client.get<HttpStatement>(url){
url {
parameters.urlEncodingOption = UrlEncodingOption.NO_ENCODING
}
headers {
append(HttpHeaders.Authorization, auth)
append(HttpHeaders.UserAgent, userAgentS)
append(HttpHeaders.Range, "bytes=${file.length()}-")
}
}
.execute { httpResponse ->
val channel: ByteReadChannel = httpResponse.receive()
while (!channel.isClosedForRead) {
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
while (!packet.isEmpty) {
val bytes = packet.readBytes()
file.appendBytes(bytes)
println("Received ${(file.length())} bytes from ${httpResponse.contentLength()}")
}
}
val pathF = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS + "/${fileName}")
file.renameTo(pathF)
println("A file saved to ${file.path}")
}
}
}

You can disable query parameters encoding by assigning the UrlEncodingOption.NO_ENCODING value to the urlEncodingOption property of the ParametersBuilder. Here is an example:
val requestBuilder = HttpRequestBuilder()
requestBuilder.url {
protocol = URLProtocol.HTTP
host = "httpbin.org"
path("get")
parameters.urlEncodingOption = UrlEncodingOption.NO_ENCODING
parameters.append("file", "/10/55/file.zip")
}
val response = client.get<String>(requestBuilder)

Related

Using nested CoroutineScopes to upload images and keeping track of them

I am a newbie to android coroutines my requirements
Need to upload 20 images
Keep track of upload(at least when it gets finished I need to hide progressBar of each image)
After uploading all the images need to enable a "next" button also
Here is my try:
private fun startUploading(){
// Get AWS data
val accessKey = sharedPreferences.getString(getString(R.string.aws_access_key), "").toString()
val secretKey = sharedPreferences.getString(getString(R.string.aws_secret_key), "").toString()
val bucketName = sharedPreferences.getString(getString(R.string.aws_bucket_name), "").toString()
val region = sharedPreferences.getString(getString(R.string.aws_region), "").toString()
val distributionUrl = sharedPreferences.getString(getString(R.string.aws_distribution_url), "").toString()
var totalImagesNeedToUpload = 0
var totalImagesUploaded = 0
CoroutineScope(Dispatchers.IO).launch {
for (i in allCapturedImages.indices) {
val allImageFiles = allCapturedImages[i].viewItem.ImageFiles
totalImagesNeedToUpload += allImageFiles.size
for (j in allImageFiles.indices) {
CoroutineScope(Dispatchers.IO).launch {
while (true) {
val internetActive = utilsClassInstance.hasInternetConnected()
if (internetActive){
try {
val file = allImageFiles[j]
if (!file.uploaded) {
// Upload the file
val cfUrl = utilsClassInstance.uploadFile(file.imageFile, accessKey, secretKey, bucketName, region, distributionUrl)
// Set the uploaded status to true
file.uploaded = true
file.uploadedUrl = cfUrl
// Increment the count of total uploaded images
totalImagesUploaded += 1
// Upload is done for that particular set image
CoroutineScope(Dispatchers.Main).launch {
mainRecyclerAdapter?.uploadCompleteForViewItemImage(i, j, cfUrl)
// Set the next button enabled
if (totalImagesUploaded == totalImagesNeedToUpload){
binding.btnNext.isEnabled = true
}
}
break
}else{
totalImagesUploaded += 1
break
}
} catch (e: Exception) {
println(e.printStackTrace())
}
}
}
CoroutineScope(Dispatchers.Main).launch {
if (totalImagesUploaded == totalImagesNeedToUpload){
updateProgressForAllImages()
binding.btnNext.isEnabled = true
}
}
}
}
}
}
}
fun uploadFile(file: File, accessKey:String, secretKey:String, bucketName: String, region:String, distributionUrl: String): String{
// Create a S3 client
val s3Client = AmazonS3Client(BasicAWSCredentials(accessKey, secretKey))
s3Client.setRegion(Region.getRegion(region))
// Create a put object
val por = PutObjectRequest(bucketName, file.name, file)
s3Client.putObject(por)
// Override the response headers
val override = ResponseHeaderOverrides()
override.contentType = "image/jpeg"
// Generate the url request
val urlRequest = GeneratePresignedUrlRequest(bucketName, file.name)
urlRequest.responseHeaders = override
// Get the generated url
val url = s3Client.generatePresignedUrl(urlRequest)
return url.toString().replace("https://${bucketName}.s3.amazonaws.com/", distributionUrl)
}
There are total "n" images that I need to upload
every image is getting uploaded in different Coroutine because I need to do the parallel upload
The whole question is how to know that all the images are uploaded and enable a next button?
Your code seems very unstructured. You have an infinite loop checking for network availability. You have a nested loop here to upload images (Why?). You are creating a lot of coroutine scopes and have no control over them
Based on the 3 requirements that you mentioned in the question, you can do something like this:
val imagesToUpload: List<File> = /* ... */
var filesUploaded = 0
lifecycleScope.launchWhenStarted {
coroutineScope { // This will return only when all child coroutines have finished
imagesToUpload.forEach { imageFile ->
launch { // Run every upload in parallel
val url = utilsClassInstance.uploadFile(file.imageFile, ...) // Assuming this is a non-blocking suspend function.
filesUploaded++
// Pass the `url` to your adapter to display the image
binding.progressBar.progress = (filesUploaded * 100) / imagesToUpload.size // Update progress bar
}
}
}
// All images have been uploaded at this point.
binding.btnNext.enabled = true
}
Ideally you should have used a viewModelScope and the upload code should be in a repository, but since you don't seem to have a proper architecture in place, I have used lifecycleScope which you can get inside an Activity or Fragment

Upload Image to Server using Multipart and Request Body in Android in Kotlin

I am stuck in between a strange issue of uploading image file to server. Although I did upload file several times before, but this time I don't understand what is the issue.
I get the file path of respective file but RequestBody returns null. Below I mentioned what library I'm using.
I am using kotlin, MultiPart and RequestBody for file upload.
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
Below is my code which I wrote for file upload. In which you can see GalleryCameraUtility.getImageRequestBody(imageFile) returns null for file.
File path from mobile device /storage/emulated/0/DCIM/Screenshots/test_image.jpg
fun addNewCompany(companyName: String, email: String,imageFile: File, ownerName: String, address: String, companyDetails: String){
val companyNameBody: RequestBody = companyName.toRequestBody("text/plain".toMediaType())
val emailBody: RequestBody = email.toRequestBody("text/plain".toMediaType())
val fileData: RequestBody? = GalleryCameraUtility.getImageRequestBody(imageFile)
val ownerNameBody: RequestBody = ownerName.toRequestBody("text/plain".toMediaType())
val addressBody: RequestBody = address.toRequestBody("text/plain".toMediaType())
val userIdBody: RequestBody = PreferenceHelper.readUserIdPref(Constants.USER_ID).toString()
.toRequestBody("text/plain".toMediaType())
addCompanyRepo.addNewCompanyApi(companyNameBody, emailBody, fileData, ownerNameBody, addressBody, userIdBody)
}
class GalleryCameraUtility {
companion object{
fun getImageRequestBody(sourceFile: File) : RequestBody? {
var requestBody: RequestBody? = null
Thread {
val mimeType = getMimeType(sourceFile);
if (mimeType == null) {
Log.e("file error", "Not able to get mime type")
return#Thread
}
try {
requestBody = sourceFile.path.toRequestBody("multipart/form-data".toMediaTypeOrNull())
/*MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart(serverImageKey, uploadedFileName,sourceFile.asRequestBody(mimeType.toMediaTypeOrNull()))
.build()*/
} catch (ex: Exception) {
ex.printStackTrace()
Log.e("File upload", "failed")
}
}.start()
return requestBody;
}
// url = file path or whatever suitable URL you want.
private fun getMimeType(file: File): String? {
var type: String? = null
val extension = MimeTypeMap.getFileExtensionFromUrl(file.path)
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
}
return type
}
}
}
I spent so many hours on this but not able to find solution. Please help me out on this.

download audio(file) from url to externalCache

I have audio 'url' "https://my-server.com/555.mp3"
how can I download it to "externalCacheDir" to be easy access without internet
i tried to use DownloadManager but i think there is better way to do it
fun downloadAudio(url:String){
val request = DownloadManager.Request(url.toUri())
val externalCachPath = getApplication<Application>().externalCacheDir!!.absolutePath
val fileName = "${UUID.randomUUID()}.3gp"
request.setDestinationInExternalPublicDir(externalCachPath,fileName)
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
request.setDescription("Download")
request.setTitle(fileName)
download(request)
return filename
}

How to POST multipart/form-data using Fuel for Kotlin?

I need to send a POST request to a server. I'm supposed to pass some parameters and an image. I am able to do this from Postman, but I can't do this on my Android app (latest SDK) using Fuel.
This is the code I'm using:
val formData = listOf("name" to "name")
val (_, _, result) = Fuel.upload("http://10.0.2.2:3000/test", parameters = formData)
.source { request, url -> imageFile } // type is File
.responseObject<CustomResponse>()
This is the postman screenshot:
I don't have access to the backend code, just some logs. It seems the request body is empty and the file also doesn't get uploaded. How can I do this? I'm at a loss.
I also tried passing the parameters as a jsonBody, this does submit the body, but the content type is not multipart/form-data and the image is still missing.
This JS code works:
let formData = new FormData();
formData.append('name', 'name');
formData.append('image', this.file);
axios.post(`${API_URL}/test`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(console.log).catch(console.log)
Edit: I also tried passing the file as a DataPart, still nothing.
After some struggle I found out what would work:
val file = FileDataPart.from("path_to_your_file", name = "image")
val (_, _, result) = Fuel.upload("http://10.0.2.2:3000/test")
.add(file)
.responseObject<CustomResponse>()
I didn't need name-name part in my case but I would try to add InlineDataPart
After some try finally I found the solution.
Try this I think it will help.
val params = listOf("email" to "test#email.com", "pass" to "123456")
Fuel.upload("http://your-api-url.com/login", Method.POST, params)
.responseString { _, _, result ->
when (result) {
is Result.Failure -> {
print(result.getException().toString())
}
is Result.Success -> {
val data = result.get()
print(data)
}
}
}
I managed to send a POST request to a server. I passed a parameter and an image.
//Prepare POST body
val postBody = listOf("name" to "name")
//Call the API.
val (_, _, result) = Fuel.upload("http://10.0.2.2:3000/test", Method.POST , postBody)
.add(BlobDataPart(myInputStream, name = "image", filename = "default.jpg", contentType = "image/jpeg"))
.responseString()
//If failed, then print exception. If successful, then print result.
when (result) {
is Result.Failure -> {
println(result.getException())
}
is Result.Success -> {
println(result.get())
}
}
You can read the related documentation here.

Display pdf in android

I have written a c# web service that returns a pdf in a stream of bytes as response. Once I make a call to the web-service from my android app, I will store the response in an array byte till here I will be able to do it. But after that I need to convert that byte array into pdf, I should be able to display that. I have a menu page in which once the button is pressed the call is made to the web service with file name and on click of button I should be able to open pdf. Is this possible? Or there is some other, better solution? I checked on the net for better understanding, but I was unable to find one that could help me understand better.
Thanks for the suggestion, but I don't have the pdf in hand, I just have the array bytes, which I got from the web service. So I now need to regenerate the pdf from this array of bytes and display it, but I am not getting how to do it.
Try following these steps
Convert byte array to InputStream
val inputStream = ByteArrayInputStream(byteArray)
Save InputStream as PDF file:
suspend fun saveInputStreamAsPdfFile(inputStream: InputStream, applicationContext: Context): File? {
var outputFile: File? = null
withContext(Dispatchers.IO) {
try {
val directory = ContextCompat.getExternalFilesDirs(applicationContext, "documents").first()
val outputDir = File(directory, "outputPath")
outputFile = File(outputDir, UUID.randomUUID().toString() + ".pdf")
if (!outputDir.exists()) {
outputDir.mkdirs()
}
val outputStream = FileOutputStream(outputFile, false)
inputStream.use { fileOut -> fileOut.copyTo(outputStream) }
outputStream.close()
} catch (e: Exception) {
// Something went wrong
}
}
return outputFile
}
Show PDF with PdfRenderer
var totalPdfPages: Int
fun showPdf(pdfFile: File) {
val input = ParcelFileDescriptor.open(pdfFile, MODE_READ_ONLY)
val renderer = PdfRenderer(input)
val wrapper = PdfRendererWrapper(renderer)
totalPdfPages = wrapper.getTotalPages()
showPdfPage(0)
}
fun showPdfPage(currentPageIndex: Int) {
val pageBitmap = wrapper.getBitmap(currentPageIndex)
imageView.setImageBitmap(pageBitmap) // Show current page
}

Categories

Resources