HOME > android > basic

안드로이드 - DownloadManager로 파일 다운받는 방법

JSFollow13 Jan 2019

DownloadManager는 HTTP 파일을 다운받는데 도와주는 시스템 서비스입니다. 앱은 저장될 위치의 URI와 다운받을 HTTP URI를 DownloadManager에 알려주기만 하면 알아서 받아줍니다. 또한, DownloadManager 내부의 백그라운드 서비스에서 다운을 받기 때문에 앱에서 쓰레드를 생성할 필요는 없습니다.

다운로드매니저는 노티피케이션에 다운로드 상태를 보여주고, 완료가 되면 브로드캐스트로 완료되었음을 알려줍니다. 또한 실시간으로 다운로드 상태를 체크할 수도 있습니다.

장점을 정리하면 다음과 같습니다.

  • 앱에서 다운로드를 위한 백그라운드 쓰레드를 만들 필요가 없습니다.
  • 노티피케이션을 따로 구성할 필요가 없습니다.
  • 다운로드가 완료되면 브로드캐스트로 알려줍니다.
  • 불안정한 네트워크 상태에 대한 예외처리가 되어있습니다. 다운로드를 실패하는 경우 다시 시도할 수 있습니다.

어떻게 다운로드매니저를 사용하는지 예제를 통해 알아보겠습니다.

완성된 예제는 GitHub에 있습니다.

UI 구성

다운로드 요청, 상태 확인, 취소 처리를 하기 위해 버튼 3개를 만들었습니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <Button
            android:id="@+id/downloadBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Download"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

    <Button
            android:id="@+id/statusBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Status"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/downloadBtn"/>

    <Button
            android:id="@+id/cancelBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Cancel"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/statusBtn"/>

</androidx.constraintlayout.widget.ConstraintLayout>

앱을 실행해보면 3개의 버튼을 갖고 있는 액티비티가 보입니다.

android downloadmanager

권한

인터넷에서 파일을 다운로드를 하기 때문에 AndroidManifest.xml에 인터넷 퍼미션을 꼭 추가해야합니다.

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

다운로드 요청(Request)

다운로드 버튼을 누르면 DownloadManager에 파일 다운로드를 요청하도록 구현하였습니다.

downloadBtn.setOnClickListener {
    downloadImage()
}

다운로드 요청은 DownloadManager.Request 객체를 생성하고 DownloadManager.enqueue를 이용하여 추가만 해주면됩니다. Request객체에 노티피케이션 등에 대한 설정 및 다운로드에 대한 제약사항을 설정할 수 있습니다.

다운로드매니저 Queue에 Request가 추가되면 다운로드매니저의 백그라운드에서 알아서 파일을 다운로드합니다. 앱에서 별도로 백그라운드 쓰레드를 만들 필요가 없습니다.

enqueue는 downloadId를 리턴하는데요. 이 Id는 다운로드 상태를 알거나 결과를 확인하기 위해 필요합니다.

private fun downloadImage() {
    val file = File(getExternalFilesDir(null), "dev_submit.mp4")
    val youtubeUrl = "https://r2---sn-oguelney.googlevideo.com/videoplayback?expire=1547361698&ratebypass=yes&ipbits=0&txp=5431432&fvip=2&sparams=clen,dur,ei,expire,gir,id,ip,ipbits,itag,lmt,mime,mip,mm,mn,ms,mv,pl,ratebypass,requiressl,source,usequic&dur=2487.205&source=youtube&id=o-AFU5WYmppmSyvhuN-vYHnA9zb_qazPL5JANaBNepI9ZF&requiressl=yes&lmt=1541762056298111&itag=18&ip=52.78.151.237&clen=106473489&signature=02FE1AD7C3C0FDFB173383A9D48A1374FDDE9470.31A7D7D3F98045946B0DD3C07436A241B223B84D&ei=Qok6XOqPGI3DqQHxlL_4CA&pl=24&key=cms1&c=WEB&gir=yes&mime=video%2Fmp4&redirect_counter=1&cm2rm=sn-oguy67l&req_id=471900e67ee3a3ee&cms_redirect=yes&mip=182.228.195.55&mm=34&mn=sn-oguelney&ms=ltu&mt=1547339611&mv=u&usequic=no"

    val request = DownloadManager.Request(Uri.parse(youtubeUrl))
        .setTitle("Downloading a video")
        .setDescription("Downloading Dev Summit")
        .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
        .setDestinationUri(Uri.fromFile(file))
        .setRequiresCharging(false)
        .setAllowedOverMetered(true)
        .setAllowedOverRoaming(true)

    downloadId = downloadManager.enqueue(request)
    Log.d(TAG, "path : " + file.path)
}

Request에 설정하는 항목은 다음과 같습니다.

  • DownloadManager.Request: Request 객체를 생성하며 인자로 다운로드할 파일의 URI를 전달합니다.
  • setTitle: 노티피케이션에 보이는 타이틀입니다.
  • setDescription: 노티피케이션에 보이는 디스크립션입니다.
  • setNotificationVisibility: VISIBILITY_VISIBLE로 설정되면 노티피케이션에 보여집니다.
  • setDestinationUri: 파일이 저장될 위치의 URI입니다.
  • setRequiresCharging: True로 설정되면, 단말이 충전중일 때만 다운로드합니다.
  • setAllowedOverMetered: True로 설정되면, 모바일네트워크가 연결되었을 때도 다운로드합니다.
  • setAllowedOverRoaming: True로 설정되면, 로밍네트워크가 연결되었을 때도 다운로드합니다.

setNotificationVisibility에 설정하는 옵션으로 아래와 같은 것들이 있습니다.

  • VISIBILITY_VISIBLE: 다운로드가 진행중일 때만 노티를 보여주며, 완료되면 보여주지 않습니다.
  • VISIBILITY_VISIBLE_NOTIFY_COMPLETED: 다운로드 진행 중 그리고 완료되었을 때 모두 노티를 보여줍니다.
  • VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION: 다운로드가 완료되었을 때만 노티를 보여줍니다.
  • VISIBILITY_HIDDEN: 노티를 보여주지 않습니다.

다운로드가 진행되면 노티피케이션으로 진행상황을 알려줍니다. 이 부분은 DownloadManager에 구현되어 있어서 따로 구현할 필요가 없습니다. android downloadmanager

다운로드 상태 확인(Status)

상태 버튼을 누르면 다운로드 상태를 토스트로 출력하도록 하였습니다.

statusBtn.setOnClickListener {
    val status = getStatus(downloadId)
    Toast.makeText(this, status, Toast.LENGTH_SHORT).show()
}

다운로드 ID만 있으면 DownloadManager에 쿼리할 수 있습니다. DownloadManager.Query객체를 생성하고 DownloadManager.query API로 쿼리를 합니다. 리턴 값으로 cursor가 리턴되며 특정 칼럼을 조회하여 다운로드 상태를 가져올 수 있습니다.

private fun getStatus(id: Long): String {
    val query: DownloadManager.Query = DownloadManager.Query()
    query.setFilterById(id)
    var cursor = downloadManager.query(query)
    if (!cursor.moveToFirst()) {
        Log.e(TAG, "Empty row")
        return "Wrong downloadId"
    }

    var columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
    var status = cursor.getInt(columnIndex)
    var columnReason = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
    var reason = cursor.getInt(columnReason)
    var statusText: String

    when (status) {
        DownloadManager.STATUS_SUCCESSFUL -> statusText = "Successful"
        DownloadManager.STATUS_FAILED -> {
            statusText = "Failed: $reason"
        }
        DownloadManager.STATUS_PENDING -> statusText = "Pending"
        DownloadManager.STATUS_RUNNING -> statusText = "Running"
        DownloadManager.STATUS_PAUSED-> {
            statusText = "Paused: $reason"
        }
        else -> statusText = "Unknown"
    }

    return statusText
}

다운로드 상태와 의미는 아래와 같습니다.

  • STATUS_SUCCESSFUL: 다운로드를 성공적으로 완료되었음
  • STATUS_FAILED: 다운로드가 실패되었음
  • STATUS_RUNNING: 현재 다운로드가 진행 중
  • STATUS_PAUSED: 다운로드가 중지되었고, 계속 받거나 다시 받기를 기다리는 상태

다운로드 상태가 STATUS_FAILED, STATUS_PAUSED일 때, 이렇게 된 이유(Reason)을 알 수 있습니다.

Reason의 종류와 아래와 같은 것들이 있습니다.

  • PAUSED_WAITING_TO_RETRY
  • PAUSED_WAITING_FOR_NETWORK
  • PAUSED_QUEUED_FOR_WIFI
  • PAUSED_UNKNOWN
  • ERROR_FILE_ERROR
  • ERROR_UNHANDLED_HTTP_CODE
  • ERROR_HTTP_DATA_ERROR
  • ERROR_TOO_MANY_REDIRECTS
  • ERROR_TOO_MANY_REDIRECTS
  • ERROR_INSUFFICIENT_SPACE
  • ERROR_DEVICE_NOT_FOUND
  • ERROR_CANNOT_RESUME
  • ERROR_FILE_ALREADY_EXISTS
  • ERROR_UNKNOWN

앱을 실행하고 다운로드 버튼을 누른 뒤 Status버튼을 누르면 Running을 출력합니다.

android downloadmanager

다운로드 취소

다운로드 요청한 것에 대해서 취소도 할 수 있습니다. DownloadManager.remove API에 인자로 downloadId를 전달하면 됩니다.

취소버튼을 누르면 요청한 Id에 대해서 취소하도록 구현하였습니다.

cancelBtn.setOnClickListener {
    if (downloadId != -1L) {
        downloadManager.remove(downloadId)
    }
}

다운로드 결과 받기, 노티 클릭 이벤트 받기

다운로드 요청이 완료되면 브로드캐스트로 결과를 전달해줍니다. 다운로드가 성공하거나 또는 실패해도 결과를 전달합니다.

사용자가 노티를 클릭하였을 때도 노티피케이션을 보내줍니다. 다운로드가 취소된 경우 다시 다운로드 받는 UI를 보여주거나, 다운받는 파일에 대한 자세한 정보를 보여주도록 구현할 수 있습니다.

먼저 DownloadManager에 다운로드를 요청하기 전에 브로드캐스트를 등록해야 합니다. DownloadManager는 다음 두개의 인텐트를 전달할 수 있습니다.

  • ACTION_DOWNLOAD_COMPLETE: 다운로드 요청이 완료(성공 또는 실패)되면 이 인텐트를 전달됩니다.
  • ACTION_NOTIFICATION_CLICKED: 사용자가 노티피케이션을 클릭하면 이 인텐트가 전달됩니다.

동적으로 브로드캐스트 리시버를 등록하였습니다.

val intentFilter = IntentFilter()
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
intentFilter.addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED)
registerReceiver(onDownloadComplete, intentFilter)

브로드캐스트 리시버는 아래와 같이 구현할 수 있습니다.

private val onDownloadComplete = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
        if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.action)) {
            if (downloadId == id) {
                val query: DownloadManager.Query = DownloadManager.Query()
                query.setFilterById(id)
                var cursor = downloadManager.query(query)
                if (!cursor.moveToFirst()) {
                    return
                }

                var columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
                var status = cursor.getInt(columnIndex)
                if (status == DownloadManager.STATUS_SUCCESSFUL) {
                    Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show()
                } else if (status == DownloadManager.STATUS_FAILED) {
                    Toast.makeText(context, "Download failed", Toast.LENGTH_SHORT).show()
                }
            }
        } else if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(intent.action)) {
            Toast.makeText(context, "Notification clicked", Toast.LENGTH_SHORT).show()
        }
    }
}

인텐트는 여러 ID에 대한 응답이 올 수 있기 때문에 downloadId를 체크해야 합니다. 다운로드가 실패하는 경우에도 인텐트가 전달되기 때문에 상태를 체크해야 합니다.

참고