[Huawei Developers] AbdurrahimCillioglu:
View attachment 5344659

Introduction

Hi everyone, In this article, we’ll explore how to develop a download manager app using the Huawei Network Kit. And, we’ll use Kotlin as a programming language in Android Studio.

Huawei Network Kit

Network Kit provides us to upload or download files with additional features such as multithreaded, concurrent, resumable uploads and downloads. Also, it allows us to perform our network operations quickly and safely. It provides a powerful interacting with Rest APIs and sending synchronous and asynchronous network requests with annotated parameters. Finally, we can use it with other Huawei kits such as hQUIC Kit and Wireless Kit to get faster network traffic.

If you want to learn how to use Network Kit with Rest APIs, you can check my article about it.

Download Manager — Sample App

In this project, we’re going to develop a download manager app that helps users download files quickly and reliably to their devices.

Key features:

Start, Pause, Resume or Cancel downloads.

Enable or Disable Sliced Download.

Set’s the speed limit for downloading a file.

Calculate downloaded size/total file size.

Calculate and display download speed.

Check the progress in the download bar.

Support HTTP and HTTPS protocols.

Copy URL from clipboard easily

We started a download task. Then, we paused and resumed it. When the download is finished, it showed a snackbar to notify us.

View attachment 5344663

Setup the Project

We’re not going to go into the details of integrating Huawei HMS Core into a project. You can follow the instructions to integrate HMS Core into your project via official docs or codelab. After integrating HMS Core, let’s add the necessary dependencies.

Add the necessary dependencies to build.gradle (app level).

dependencies {
...
// HMS Network Kit
implementation 'com.huawei.hms:filemanager:5.0.3.300'
// For runtime permission
implementation 'androidx.activity:activity-ktx:1.2.3'
implementation 'androidx.fragment:fragment-ktx:1.3.4'
...
}

Let’s add the necessary permissions to our manifest.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.huawei.networkkitsample">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>

We added the Internet Permission to access the Internet and the storage permissions to read and write data to the device memory. Also, we will dynamically request the permissions at runtime for storage permissions on devices that runs Android 6.0 (API Level 23) or higher.

Configure the AndroidManifest file to use clear text traffic

If you try to download a file from an HTTP URL on Android 9.0 (API level 28) or higher, you’ll get an error like this:

ErrorCodeFromException errorcode from resclient: 10000802,message:CLEARTEXT communication to ipv4.download.thinkbroadband.com(your url) not permitted by network security policy

Because cleartext support is disabled by default on Android 9.0 or higher. You should add the android:usesClearTextTraffic="true" flag in the AndroidManifest.xml file. If you don’t want to enable it for all URLs, you can create a network security config file. If you are only working with HTTPS files, you don’t need to add this flag.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.huawei.networkkitsample">
...
<application
...
android:usesCleartextTraffic="true"
...
</application>
</manifest>

Layout File

View attachment 5344665

activity_main.xml is the only layout file in our project. There are:

A TextInputEditText to enter URL,

Four buttons to control the download process,

A button to paste URL to the TextInputEditText,

A progress bar to show download status,

A seekbar to adjust download speed limit,

A checkbox to enable or disable the “Slide Download” feature,

TextViews to show various information.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<Button
android:id="@+id/startDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Start"
app:layout_constraintEnd_toStartOf="@+id/pauseDownload_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/enableSliced_checkBox" />
<ProgressBar
android:id="@+id/downloadProgress_progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:progressBackgroundTint="@color/design_default_color_primary_variant"
android:progressTint="@color/design_default_color_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/percentProgress_textView" />
<TextView
android:id="@+id/percentProgress_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="0%"
app:layout_constraintStart_toStartOf="@+id/downloadProgress_progressBar"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
<TextView
android:id="@+id/finishedSize_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="0"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/percentProgress_textView"
tools:text="2.5" />
<TextView
android:id="@+id/sizeSeparator_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="/"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/finishedSize_textView" />
<TextView
android:id="@+id/totalSize_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="0"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/sizeSeparator_textView"
tools:text="29.6 MB" />
<SeekBar
android:id="@+id/speedLimit_seekBar"
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:max="7"
android:progress="7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fixSpeedLimit_textView" />
<TextView
android:id="@+id/fixSpeedLimit_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="Download Speed Limit:"
app:layout_constraintStart_toStartOf="@+id/speedLimit_seekBar"
app:layout_constraintTop_toBottomOf="@+id/remainingTime_textView" />
<TextView
android:id="@+id/speedLimit_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Limitless"
app:layout_constraintBottom_toBottomOf="@+id/fixSpeedLimit_textView"
app:layout_constraintStart_toEndOf="@+id/fixSpeedLimit_textView" />
<TextView
android:id="@+id/currentSpeed_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 kB/s"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintEnd_toEndOf="@+id/downloadProgress_progressBar"
tools:text="912 kB/s" />
<Button
android:id="@+id/pauseDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pause"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/startDownload_button"
app:layout_constraintTop_toTopOf="@+id/startDownload_button" />
<Button
android:id="@+id/resumeDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Resume"
app:layout_constraintEnd_toEndOf="@+id/startDownload_button"
app:layout_constraintStart_toStartOf="@+id/startDownload_button"
app:layout_constraintTop_toBottomOf="@+id/startDownload_button" />
<Button
android:id="@+id/cancelDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Cancel"
app:layout_constraintEnd_toEndOf="@+id/pauseDownload_button"
app:layout_constraintStart_toStartOf="@+id/pauseDownload_button"
app:layout_constraintTop_toBottomOf="@+id/pauseDownload_button" />
<TextView
android:id="@+id/remainingTime_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0s left"
app:layout_constraintStart_toStartOf="@+id/downloadProgress_progressBar"
app:layout_constraintTop_toBottomOf="@+id/downloadProgress_progressBar" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/pasteClipboard_imageButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/url_textInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="URL"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/pasteClipboard_imageButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:background="@android:color/transparent"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textInputLayout"
app:srcCompat="@drawable/ic_paste_content" />
<CheckBox
android:id="@+id/enableSliced_checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:checked="true"
android:text="Enable Slice Download"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/speedLimit_seekBar" />
</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity

Let’s interpret some of the functions on this page.

onCreate() - Firstly we used viewBinding instead of findViewById. It generates a binding class for each XML layout file present in that module. With the instance of a binding class, we can access the view hierarchy with type and null safety.

Then, we initialized the ButtonClickListeners and the ViewChangeListeners. And we create a FileRequestCallback object. We’ll go into the details of this object later.

startDownloadButton() - When the user presses the start download button, it requests permissions at runtime. If the user allows accessing device memory, it will start the download process.

startDownload() - First, we check the downloadManager is initialized or not. Then, we check if there is a download task or not. getRequestStatus function provides us the result status as INIT, PROCESS, PAUSE and, INVALID.

If auto-import is active in your Android Studio, It can import the wrong package for the Result Status. Please make sure to import the "com.huawei.hms.network.file.api.Result" package.

The Builder helps us to create a DownloadManager object. We give a name to our task. If you plan to use the multiple download feature, please be careful to give different names to your download managers.

The DownloadManagerBuilder helps us to create a DownloadManager object. We give a tag to our task. In our app, we only allow single downloading to make it simple. If you plan to use the multiple download feature, please be careful to give different tags to your download managers.

When creating a download request, we need a file path to save our file and a URL to download. Also, we can set a speed limit or enable the slice download.

Currently, you can only set the speed limit for downloading a file. The speed limit value ranges from 1 B/s to 1 GB/s. speedLimit() takes a variable of the type INT as a byte value.

You can enable or disable the sliced download.

Sliced Download: It slices the file into multiple small chunks and downloads them in parallel.

Finally, we start an asynchronous request with downloadManager.start() command. It takes the getRequest and the fileRequestCallback.

FileRequestCallback object contains four callback methods: onStart, onProgress, onSuccess and onException.

onStart -> It will be called when the file download starts. We take the startTime to calculate the remaining download time here.

onProgress -> It will be called when the file download progress changes. We can change the progress status here.

These methods run asynchronously. If we want to update the UI, we should change our thread to the UI thread using the runOnUiThread methods.

onSuccess -> It will be called when file download is completed. We show a snackbar to the user after the file download completes here.

onException -> It will be called when an exception occurs.

onException also is triggered when the download is paused or resumed. If the exception message contains the "10042002" number, it is paused, if it contains the "10042003", it is canceled.

MainActivity.kt

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var downloadManager: DownloadManager
private lateinit var getRequest: GetRequest
private lateinit var fileRequestCallback: FileRequestCallback
private val TAG = "MainActivity"
private var downloadURL = "http://ipv4.download.thinkbroadband.com/20MB.zip"
private var downloadSpeedLimit: Int = 0
private var startTime: Long = 0L
private var isEnableSlicedDownload = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.urlTextInputEditText.setText(downloadURL)
initButtonClickListeners()
initViewChangeListeners()
fileRequestCallback = object : FileRequestCallback() {
override fun onStart(getRequest: GetRequest): GetRequest {
startTime = System.nanoTime()
return getRequest
}
override fun onProgress(getRequest: GetRequest, progress: Progress) {
runOnUiThread {
binding.downloadProgressProgressBar.progress = progress.progress
binding.percentProgressTextView.text = "${progress.progress}%"
convertByteToMb(progress.totalSize)?.let {
binding.totalSizeTextView.text = "$it MB"
}
convertByteToMb(progress.finishedSize)?.let {
binding.finishedSizeTextView.text = it
}
showCurrentDownloadSpeed(progress.speed)
showRemainingTime(progress)
}
}
override fun onSuccess(response: Response<GetRequest, File, Closeable>?) {
if (response?.content != null) {
runOnUiThread {
binding.downloadProgressProgressBar.progress = 100
binding.percentProgressTextView.text = "100%"
binding.remainingTimeTextView.text = "0s left"
convertByteToMb(response.content.length())?.let {
binding.finishedSizeTextView.text = it
binding.totalSizeTextView.text = "$it MB"
}
showSnackBar(binding.mainConstraintLayout, "Download Completed")
}
}
}
override fun onException(
getRequest: GetRequest?,
exception: NetworkException?,
response: Response<GetRequest, File, Closeable>?
) {
if (exception != null) {
val pauseTaskValue = "10042002"
val cancelTaskValue = "10042003"
val errorMessage = exception.message
errorMessage?.let {
if (!it.contains(pauseTaskValue) && !it.contains(cancelTaskValue)) {
Log.e(TAG, "Error Message:$it")
exception.cause?.let { throwable ->
runOnUiThread {
Toast.makeText(
this@MainActivity,
throwable.message,
Toast.LENGTH_SHORT
)
.show()
}
}
}
}
}
}
}
}
private fun initViewChangeListeners() {
binding.speedLimitSeekBar.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
downloadSpeedLimit = calculateSpeedLimitAsByte(progress)
showDownloadSpeedLimit(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
}
})
binding.enableSlicedCheckBox.setOnCheckedChangeListener { _, isChecked ->
isEnableSlicedDownload = isChecked
}
}
private fun initButtonClickListeners() {
binding.startDownloadButton.setOnClickListener {
activityResultLauncher.launch(
arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
)
}
binding.pauseDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) return@setOnClickListener
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PROCESS -> {
downloadManager.pauseRequest(getRequest.id)
}
else -> {
Toast.makeText(this, "No valid download request", Toast.LENGTH_SHORT).show()
}
}
}
binding.resumeDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) return@setOnClickListener
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PAUSE -> {
downloadManager.resumeRequest(getRequest, fileRequestCallback)
}
else -> {
Toast.makeText(this, "No download process", Toast.LENGTH_SHORT).show()
}
}
}
binding.cancelDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) return@setOnClickListener
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PROCESS -> {
downloadManager.cancelRequest(getRequest.id)
clearAllViews()
}
Result.STATUS.PAUSE -> {
downloadManager.cancelRequest(getRequest.id)
clearAllViews()
}
else -> {
Toast.makeText(this, "No valid download request", Toast.LENGTH_SHORT).show()
}
}
}
binding.pasteClipboardImageButton.setOnClickListener {
pasteClipboardData()
}
}
private val activityResultLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
)
{ permissions ->
val allGranted = permissions.entries.map {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
checkSelfPermission(it.key)
} else {
true
}
}.map { it == PackageManager.PERMISSION_GRANTED }.find { !it } ?: true
if (!allGranted) {
Toast.makeText(this, "Permission are not granted", Toast.LENGTH_SHORT).show()
} else {
startDownload()
}
}
private fun startDownload() {
if (this::downloadManager.isInitialized) {
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PAUSE -> {
Toast.makeText(
this,
"Press Resume Button to continue download process",
Toast.LENGTH_SHORT
).show()
return
}
Result.STATUS.PROCESS -> {
Toast.makeText(
this,
"First cancel the current download process",
Toast.LENGTH_SHORT
).show()
return
}
}
}
downloadManager = DownloadManager.Builder("downloadManager")
.build(this)
val fileName = downloadURL.substringAfterLast("/")
val downloadFilePath = this.cacheDir.path + File.separator + fileName
val currentDownloadURL = binding.urlTextInputEditText.text.toString()
getRequest = DownloadManager.newGetRequestBuilder()
.filePath(downloadFilePath)
.url(currentDownloadURL)
.speedLimit(downloadSpeedLimit)
.enableSlice(isEnableSlicedDownload)
.build()
val result = downloadManager.start(getRequest, fileRequestCallback)
if (result.code != Result.SUCCESS) {
Log.d(TAG, "An Error occurred when downloading")
}
}
private fun convertByteToMb(sizeInByte: Long): String? {
return if (sizeInByte < 0 || sizeInByte == 0L) {
null
} else {
val sizeInMb: Float = sizeInByte / (1024 * 1024).toFloat()
String.format("%.2f", sizeInMb)
}
}
private fun showCurrentDownloadSpeed(speedInByte: Long) {
val downloadSpeedText = if (speedInByte <= 0) {
"-"
} else {
val sizeInKb: Float = speedInByte / 1024.toFloat()
String.format("%.2f", sizeInKb) + "kB/s"
}
binding.currentSpeedTextView.text = downloadSpeedText
}
private fun calculateSpeedLimitAsByte(progressBarValue: Int): Int {
return when (progressBarValue) {
0 -> 512 * 1024
1 -> 1024 * 1024
2 -> 2 * 1024 * 1024
3 -> 4 * 1024 * 1024
4 -> 6 * 1024 * 1024
5 -> 8 * 1024 * 1024
6 -> 16 * 1024 * 1024
7 -> 0
else -> 0
}
}
private fun showDownloadSpeedLimit(progressValue: Int) {
val message = when (progressValue) {
0 -> "512 kB/s"
1 -> "1 mB/s"
2 -> "2 mB/s"
3 -> "4 mB/s"
4 -> "6 mB/s"
5 -> "8 mB/s"
6 -> "16 mB/s"
7 -> "Limitless"
else -> "Error"
}
binding.speedLimitTextView.text = message
}
private fun isDownloadManagerInitialized(): Boolean {
return if (this::downloadManager.isInitialized) {
true
} else {
Toast.makeText(this, "First start the download", Toast.LENGTH_SHORT).show()
false
}
}
private fun pasteClipboardData() {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = clipboardManager.primaryClip
val clipItem = clipData?.getItemAt(0)
val text = clipItem?.text.toString()
if (text == "null") {
Toast.makeText(this, "There is no text on clipboard", Toast.LENGTH_SHORT).show()
} else {
binding.urlTextInputEditText.setText(text)
}
}
private fun showRemainingTime(progress: Progress) {
val elapsedTime = System.nanoTime() - startTime
val allTimeForDownloading =
(elapsedTime * progress.totalSize / progress.finishedSize)
val remainingTime = allTimeForDownloading - elapsedTime
val hours = TimeUnit.NANOSECONDS.toHours(remainingTime)
val minutes = TimeUnit.NANOSECONDS.toMinutes(remainingTime) % 60
val seconds = TimeUnit.NANOSECONDS.toSeconds(remainingTime) % 60
val remainingTimeAsText = if (hours > 0) {
"${hours}h ${minutes}m ${seconds}s left"
} else {
if (minutes > 0) {
"${minutes}m ${seconds}s left"
} else {
"${seconds}s left"
}
}
binding.remainingTimeTextView.text = remainingTimeAsText
}
private fun showSnackBar(rootView: View, message: String) {
val snackBar = Snackbar.make(rootView, message, Snackbar.LENGTH_SHORT)
snackBar.show()
}
private fun clearAllViews() {
binding.percentProgressTextView.text = "0%"
binding.finishedSizeTextView.text = "0"
binding.totalSizeTextView.text = "0"
binding.currentSpeedTextView.text = "0 kB/s"
binding.downloadProgressProgressBar.progress = 0
binding.remainingTimeTextView.text = "0s left"
}
}

Tips & Tricks

According to the Wi-Fi status awareness capability of the Huawei Awareness Kit, you can pause or resume your download task. It will reduce the cost to the user and help to manage your download process properly.

Before starting the download task, you can check that you’re connected to the internet using the ConnectivityManager.

If the download file has the same name as an existing file, it will overwrite the existing file. Therefore, you should give different names for your files.

Even if you minimize the application, the download will continue in the background.

Conclusion

In this article, we have learned how to use Network Kit in your download tasks. And, we’ve developed the Download Manager app that provides many features. In addition to these features, you can also use Network Kit in your upload tasks. Please do not hesitate to ask your questions as a comment.

Thank you for your time and dedication. I hope it was helpful. See you in other articles.

References

Huawei Network Kit Official Documentation

Huawei Network Kit Official Codelab

Huawei Network Kit Official Github