Post

Using Range Requests in Android

While working with big files, there are times when we would like to access a specific part of it. This is usually not an issue when the file is present locally, as devices nowadays have an incredible r/w speed allowing us to quickly parse the file in question. However, it becomes completely different when the file is hosted on a remote server. In that case, parsing/draining the big file isn’t a good option as that can lead to a waste of data and be incredibly slow, depending upon the speed of the data connection.

Accessing a file is usually done with a ContentUris nowadays, as that works everywhere (but doesn’t gives us an absolute file path). We can open an InputStream to read data from it and OutputStream to save it into another file.

What is the range request

Quoting from Mozilla’s article here:

An HTTP range request asks the server to send only a portion of an HTTP message back to a client. Range requests are useful for clients like media players that support random access, data tools that know they need only part of a large file, and download managers that let the user pause and resume the download.

The ability to request only a portion of the file makes the range request useful. However, the Range header that is used for this functionality requires us to specify the exact start and end bytes of the file which we want to request and the server must also support the range request functionality.

Working with files using range request

Reading a file from a remote server using a range request is quite easy. While opening an HttpsURLConnection or HttpURLConnection, specify the Range header.

Making network request and working with them can block the main thread is not recommended. Use Kotlin Coroutines’s IO dispatcher for such purposes.

The output might have whitespaces and multiple lines per the case. Ensure to handle those after reading the said data.

1
2
3
4
5
6
7
8
9
10
11
try {
    val connection = URL(url).openConnection() as HttpsURLConnection
    // Request a specific range to avoid skipping through load of data
    connection.setRequestProperty(
        "Range",
        "bytes=${packageFile.offset}-${packageFile.offset + packageFile.size}"
    )
    val properties = connection.inputStream.bufferedReader().use { it.readText() }
} catch (exception: Exception) {
    Log.e(TAG, "Failed fetching properties!", exception)
}

Similarly, you can also download parts of the file by saving the data from InputStream to an OutputStream of a local file.

The system/library might automatically append or remove additional bytes during download. Handle those manually by incrementing or decrementing as per the use case. The example below already does such for convenience purposes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val metadataFile = File("${context.filesDir.absolutePath}/${packageFile.filename}")
try {
    metadataFile.createNewFile()
    val connection = URL(url).openConnection() as HttpsURLConnection
    // Request a specific range to avoid skipping through load of data
    // Also do a [-1] to the range end to adjust the file size
    connection.setRequestProperty(
        "Range",
        "bytes=${packageFile.offset}-${packageFile.offset + packageFile.size - 1}"
    )
    connection.inputStream.use { input ->
        metadataFile.outputStream().use { input.copyTo(it) }
    }
} catch (exception: Exception) {
    Log.e(TAG, "Failed to download metadata file! ", exception)
}

That should be all one may need to read and download files using range request on Android.

This post is licensed under CC BY 4.0 by the author.