Download Files in Kotlin for Android Using Ktor and Intents

How to stop unwanted downloads on android smartphones
How to stop unwanted downloads on android smartphones

Article summary

Giving a user the ability to download files in your app can be difficult to figure out. In iOS, you can use AlamoFire to download the file locally and then present it with a UIDocumentInteractionController. (The code would look something like this.) It presents the documents, images, gifs, videos, etc. in the app for you, and you can then download to the device from there.

Unfortunately, this isn’t simple in Android because there are many OEMs for Android devices. (In-app image viewing can be handled using Glide. If you absolutely need to view PDFs in-app, you can probably find some solution, but I would recommend just giving the users download ability and letting their device handle it.) Today, I’ll explain how to download files in Kotlin using Ktor and intents.

Initial Setup

The first thing you will need are some dependencies, Ktor, and coroutines. Add these to your app Gradle. Ktor allows for asynchronous communication, which is very useful for file downloading and reporting file progress.

dependencies { ... implementation "io.ktor:ktor-client-android:1.2.5" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3' ... }

Then we need to add this file into res.xml. (You’ll likely need to create the xml resources folder.) It adds an external path.

<paths> <external-path name="external_files" path="."/> </paths>

In the AndroidManifest, make sure to add these permissions:

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Add the provider. This uses the external path we defined above for the FileProvider.

<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/external_files"/> </provider>

Downloader Coroutine

The coroutine for downloading files will be an extension on Ktor’s HttpClient. First, we need a class to return during the coroutine to report on the status of the download.

sealed class DownloadResult { object Success : DownloadResult() data class Error(val message: String, val cause: Exception? = null) : DownloadResult() data class Progress(val progress: Int): DownloadResult() }

This extension creates a coroutine that takes an output stream and URL. While the file is read in, the current progress is emitted. Once finished, the data is written to the output stream, and success is returned. Otherwise, there was some failure.

suspend fun HttpClient.downloadFile(file: OutputStream, url: String): Flow<DownloadResult> { return flow { try { val response = call { url(url) method = HttpMethod.Get }.response val data = ByteArray(response.contentLength()!!.toInt()) var offset = 0 do { val currentRead = response.content.readAvailable(data, offset, data.size) offset += currentRead val progress = (offset * 100f / data.size).roundToInt() emit(DownloadResult.Progress(progress)) } while (currentRead > 0) response.close() if (response.status.isSuccess()) { withContext(Dispatchers.IO) { file.write(data) } emit(DownloadResult.Success) } else { emit(DownloadResult.Error("File not downloaded")) } } catch (e: TimeoutCancellationException) { emit(DownloadResult.Error("Connection timed out", e)) } catch (t: Throwable) { emit(DownloadResult.Error("Failed to connect")) } } }

This code was originally found on Kotlin Academy.

  Fast Download Manager for Android Free

The ViewModel and Layout

Before making the fragment, we need a view model and a layout. The view model is simple, only containing a Boolean to indicate if the download is occurring.

class MainViewModel : ViewModel() { private val _downloading: MutableLiveData<Boolean> = MutableLiveData() val downloading: LiveData<Boolean> = _downloading fun setDownloading(downloading: Boolean) { _downloading.value = downloading } }

The layout itself is also simple, just a button (which is enabled when the file isn’t downloading) and a horizontal progress bar. Note that material-style circle progress bars can’t have progress set and continually spin; they’re more useful for other types of requests.

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewModel" type="com.example.kotlin_file_download.MainViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <androidx.appcompat.widget.AppCompatButton android:id="@+id/view_button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:layout_marginEnd="40dp" android:text="Download File" android:enabled="@{!safeUnbox(viewModel.downloading)}" app:layout_constraintVertical_chainStyle="packed" app:layout_constraintBottom_toTopOf="@+id/progress_bar" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ProgressBar android:id="@+id/progress_bar" style="@android:style/Widget.Material.ProgressBar.Horizontal" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:indeterminate="false" android:max="100" android:progress="0" android:progressTint="@color/colorPrimary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/view_button" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>

The Fragment

The base of the fragment is straightforward enough. Of note are the permissions and codes: if the app doesn’t have these permissions, the app can’t download files. The codes don’t need to be “1” and “2,” but if there are others in the app, they should all be unique.

  How to Stop Current Downloads in Android

When the fragment loads, if we have permissions, we can set the button click listener; otherwise, we have to request permission. Some apps request these permissions on first load if they require them throughout the app. Others only request them where they are actually needed. If your app has a specific place where file downloads occur, you can just request permission there.

class MainFragment : Fragment() { private lateinit var binding: FragmentMainBinding private lateinit var viewModel: MainViewModel private val PERMISSIONS = listOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) private val PERMISSION_REQUEST_CODE = 1 private val DOWNLOAD_FILE_CODE = 2 private val fileUrl = "https://d2v9y0dukr6mq2.cloudfront.net/video/thumbnail/rcxbst_b0itvu9rs2/kitten-in-a-cup-turns-its-head-and-watches_raeb_02je_thumbnail-full01.png" override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = DataBindingUtil.inflate( inflater, R.layout.fragment_main, container, false ) binding.lifecycleOwner = viewLifecycleOwner return binding.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) if (hasPermissions(context, PERMISSIONS)) { setDownloadButtonClickListener() } else { requestPermissions(PERMISSIONS.toTypedArray(), PERMISSION_REQUEST_CODE) } } ... }

First, we need functions to check the permissions and the request result. Note that only devices with Marshmallow (API 23) or later need permissions; earlier devices had them by default.

private fun hasPermissions(context: Context?, permissions: List<String>): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null) { return permissions.all { permission -> ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED } } return true } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == PERMISSION_REQUEST_CODE && hasPermissions(context, PERMISSIONS)) { setDownloadButtonClickListener() } }

If permission needs to be requested, our return should set the click listener when permission is granted. Either you can force the download to occur where you want with the name you provide, or you can let the user decide the name and location. I use the latter case here; the click will start an intent for creating a document with some defaults set.

private fun setDownloadButtonClickListener() { val folder = context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) val fileName = "kitten_in_a_cup.png" val file = File(folder, fileName) val uri = context?.let { FileProvider.getUriForFile(it, "${BuildConfig.APPLICATION_ID}.provider", file) } val extension = MimeTypeMap.getFileExtensionFromUrl(uri?.path) val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) binding.viewButton.setOnClickListener { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.setDataAndType(uri, mimeType) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP intent.putExtra(Intent.EXTRA_TITLE, fileName) intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) intent.addCategory(Intent.CATEGORY_OPENABLE) startActivityForResult(intent, DOWNLOAD_FILE_CODE) } }

The result will use the returned URI as the location for downloading the file.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == DOWNLOAD_FILE_CODE && resultCode == Activity.RESULT_OK) { data?.data?.let { uri -> context?.let { context -> downloadFile(context, fileUrl, uri) } } } }

Downloading the file opens the output stream to the URI given and dispatches the download file coroutine. The download itself is handled on the IO thread, but the emitter results are handled on the Main thread. This allows for the correct asynchronous updates (in this case, updating the progress bar).

private fun downloadFile(context: Context, url: String, file: Uri) { val ktor = HttpClient(Android) viewModel.setDownloading(true) context.contentResolver.openOutputStream(file)?.let { outputStream -> CoroutineScope(Dispatchers.IO).launch { ktor.downloadFile(outputStream, url).collect { withContext(Dispatchers.Main) { when (it) { is DownloadResult.Success -> { viewModel.setDownloading(false) binding.progressBar.progress = 0 viewFile(file) } is DownloadResult.Error -> { viewModel.setDownloading(false) Toast.makeText( context, "Error while downloading file", Toast.LENGTH_LONG ).show() } is DownloadResult.Progress -> { binding.progressBar.progress = it.progress } } } } } } }

The viewFile function takes the same URI given and opens it in an intent, if able. If there are multiple applications where the file can be viewed, it presents a chooser.

private fun viewFile(uri: Uri) { context?.let { context -> val intent = Intent(Intent.ACTION_VIEW, uri) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val chooser = Intent.createChooser(intent, "Open with") if (intent.resolveActivity(context.packageManager) != null) { startActivity(chooser) } else { Toast.makeText(context, "No suitable application to open file", Toast.LENGTH_LONG).show() } } }

The code for this can be found on my Github repo: kotlin-file-downloading.

  Download Notifications actions

iOS Code Sample

func downloadFile(fileURL: URL, dispatchQueue: DispatchQueue) { viewButton?.isEnabled = false startActivityIndicator() let destination: DownloadRequest.DownloadFileDestination = { _, _ in let documentsURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] if let name = self.document?.name, let type = self.document?.fileType?.lowercased() { let fileURL = documentsURL.appendingPathComponent("\(name).\(type)") return (fileURL, [.removePreviousFile, .createIntermediateDirectories]) } let fileURL = documentsURL.appendingPathComponent(fileURL.lastPathComponent) return (fileURL, [.removePreviousFile, .createIntermediateDirectories]) } print("downloading file \(fileURL.absoluteString)") Alamofire.download(fileURL, to: destination).response(queue: dispatchQueue) { response in DispatchQueue.main.async { self.stopActivityIndicator() self.viewButton?.isEnabled = true if let error = response.error { self.handleCommonAPIErrors(error, alertPresenter: self.alertPresenter) } else if let fileUrl = response.destinationURL { self.docController = UIDocumentInteractionController(url: fileUrl) self.docController!.delegate = self if !self.docController!.presentPreview(animated: true) { self.fileAvailableForViewing(viewable: false) } } else { self.alertPresenter.alertMessage(AlertText.failedDownload, title: AlertText.genericErrorTitle, onTopOf: self, buttonTitle: nil, handler: nil) } } } }

You are viewing this post: Download Files in Kotlin for Android Using Ktor and Intents. Informated by Giáo Dục Việt Á selection and synthesis along with other related topics.

Related Posts

10 Easy Ways to Fix the Play Store Download Pending Error on Android

10 Easy Ways to Fix the Play Store Download Pending Error on Android

How to manually delete downloaded files from android to make some space free How to manually delete downloaded files from android to make some space free Why…

I accidentally started the download/install process for the Android 11 beta, can I cancel it?

I accidentally started the download/install process for the Android 11 beta, can I cancel it?

How To Delete Everything From ANY Android Phone! (2021) How To Delete Everything From ANY Android Phone! (2021) I have two Pixel phones on Android 10. Yesterday…

Download Notifications actions

Download Notifications actions

How To Downgrade Any App On Android! (2020) How To Downgrade Any App On Android! (2020) I have an app that in part of this app downloads…

Fast Download Manager for Android Free

Fast Download Manager for Android Free

How To Cancel Downloads on Microsoft Store How To Cancel Downloads on Microsoft Store Manage all your download from a single screen Android is the most widely…

How Do I Stop Chrome from Downloading a File in an Android

How Do I Stop Chrome from Downloading a File in an Android

Chrome stuck on download. Fix! | Chrome endless download. Problem solve! Chrome stuck on download. Fix! | Chrome endless download. Problem solve! Google Chrome is a popular…

How to Cancel a Google Play Store or Android App Subscription

How to Cancel a Google Play Store or Android App Subscription

How to Pause Google Drive Upload/Download Android |Tricks You Didn’t Know! How to Pause Google Drive Upload/Download Android |Tricks You Didn’t Know! The Google Play Store is…

Leave a Reply

Your email address will not be published. Required fields are marked *