Download File into app specific storage with Retrofit

 In the previous post, DownloadManager is used to download files into the device’s public directory. If the file contains sensitive data, we hope the file can be saved into the app-specific storage rather than public directory. Therefore, we need another method to download. Retrofit is a type-safe  HTTP client which help us transfer information and data over a network.

Dependency

Add below dependency into module build.gradle file.

implementation("com.squareup.retrofit2:retrofit:2.9.0")

implementation("com.squareup.retrofit2:converter-gson:2.9.0")


We add codes on the same module of DownloadManager. A button is added next to the DownLoadManager Button. When it is clicked, it will start to download the file using Retrofit.


Implementation

We first create a interface so that Retrofit can translate the java/Kotlin function to HTTP API.  The notation @Streaming is used for downloading large file so that it would not write too large data into memory at once.

The download link is https://drive.google.com/uc?export=download&id=1XSTImggtvJ8oZYIB8sEByMuZipiabCaS

The base url is https://drive.google.com/ and it will be set when we create the instance of Retrofit. “uc” is the fixed suffix so we put it inside the parentheses of notation @GET. There is a query for 2 parameters “export” and “id”, which are also seted as the input parameter for the Java/ Kotlin function. Once we know the download id of a file in google drive, we can use this function to download the file. Since the direct download link is used here, the return type of the call is ResponseBody.

interface DownloadFileService {
   @Streaming
   @GET("uc")
   fun downloadFile(@Query("export") export: String, @Query("id") id: String): Call<ResponseBody>
}

Then we create a Singleton object where it keeps the Retrofit object. Singleton is used here so that the Retrofit object with base Url of google drive can be invoked throughout the module.
object RetrofitServiceCreatorToHomeWebsite {
    private const val BASE_URL = "https://drive.google.com/"
    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        //.addConverterFactory(GsonConverterFactory.create())
        .build()


    fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)

}

OnClickListener is added for the button added at the first step and a function is invoked to download the file.
val retrofitDownloadbutton: Button = findViewById(R.id.retrofitDownloadButton)
retrofitDownloadbutton.setOnClickListener {
    Log.d(TAG, "click Retrofit download button")
    startDownloadWithRetrofit()
}

In this function, the object responsible for HTTP API is retrieved. The query parameter is entered. The access request is enqueue for connection with a callback. In callback, The location to save the file is specified here  and is passed to a function with response.body(). Remember to invoke the function inside coroutine to avoid blocking the main thread.
fun startDownloadWithRetrofit(){

    val downloadFileService: DownloadFileService = RetrofitServiceCreatorToHomeWebsite.create(DownloadFileService::class.java)
    val call = downloadFileService.downloadFile(downloadLineExport, downloadLinkId)


    call.enqueue(object: Callback<ResponseBody> {
        override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>){
            if(response.isSuccessful){
                Log.d(TAG, "The file is found on server")
                //indicate the download location
                val customDirectory = File(applicationContext.cacheDir, "custom")
                if( !customDirectory.exists())
                    customDirectory.mkdir()
                val filePath = File(customDirectory, fileName)

                networkRequestScope.launch {
                    writeResponseBodyToStorage(response.body()!!, filePath)

                    Log.d(TAG, "end download. File is saved on ${filePath.absolutePath}")
                }
                Log.d(TAG, "Callback.onResponse end")
            }else{
                Log.e(TAG, "fail to connect server: " + response.toString())
            }
        }


        override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
            Log.e(TAG, "Request Download Error", t)
        }
    })
}

InputStream and OutputStream are used. The total size of the download file can be obtained by invoking ResponseBody.contentlength() before the actual download progress starts. We can check if the device has enough free space to save the file or abort the download (not implement yet). ByteArray is created as buffer and we can count how many bytes have been downloaded. We could create a Notification here to show the download progress (not implement yet)
suspend fun writeResponseBodyToStorage(body: ResponseBody, path: File){

    var inputStream: InputStream? = null
    var outputStream: OutputStream? = null
    val fileSize = body.contentLength()
    var byteDownloaded = 0


    Log.d(TAG, "file will save to ${path.absoluteFile} with size $fileSize")
    try {
        inputStream = body.byteStream()
        outputStream = FileOutputStream(path)
        val buffer = ByteArray(4 * 1024) //allocate 4k Byte buffer
        while (true) {
            val byteRead = inputStream.read(buffer)
            if (byteRead < 0)
                break
            outputStream.write(buffer, 0, byteRead)
            byteDownloaded += byteRead
            Log.d(TAG, "download progress $byteDownloaded of $fileSize")
        }
        outputStream.flush()


    } catch (ex: IOException) {
        Log.e(TAG, "fail to save file", ex)
    } finally {
        inputStream?.close()
        outputStream?.close()
    }

}

Logcat shows the download progress and the path of the download file in the device

We can find the file at the destination by browsing “Device Explorer” of the emulated device.

留言

此網誌的熱門文章

Use okhttp to download file and show progress bar

Unzipp file with Zip4j library