본문 바로가기

안드로이드/코틀린

Webview 파일 다운로드 구현하기

반응형

웹뷰 관련 포스팅

웹뷰에서 다운로드 기능을 별도로 구현해야 합니다. 

webview.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
	//.. 구현 
}

위 코드 처럼 웹뷰에 리스너를 추가해 다운로드 타입으로 설정된 버튼등을 클릭했을때 별도 처리를 해야합니다.

엑티비티에서 사용중인 webview에 설정해도 되지만 웹뷰를 커스텀뷰로 생성해서 공통적으로 사용하면 설정 값이 변경되거나 추가할때 편리합니다. ( 유지보수성 용이 ) 

커스텀뷰 생성은 제 포스팅을 참고해주세요

본론으로 돌아가서 다운로드를 클릭했을때 호출된 리스너의 파라미터들의 정보들은 각

  • url : 다운로드 url
  • userAgent : 웹뷰에 설정된 기기 정보 및 브라우저 정보들이 동일하게 담겨 있습니다.
  • contentDispostion : 실질적인 파일 이름인데, 한글일 경우 URL 인코딩 되어 있기 때문에 디코딩을 해서 사용해야 합니다.
  • 그리고 파일 명들에 부수적으로 붙어 있는 텍스트들이 있는데 그런 부분들을 replace등으로 제거 후 순수 파일 이름만으로 다운로드 해야 합니다.
  • mimetype: 다운로드 하는 파일의 타입으로 pdf, jpg등 입니다. 

| 권한 설정 

웹뷰의 인터넷 권한과 더불어 파일 저장에 필요한 권한을 추가합니다.

Androidmanifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="packageName">

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

| 다운로드 리스너 

파일을 다운로드 하기전에 파일 읽기 쓰기 권한이 체크를 하지 않으면 에러가 발생합니다.  

그리고 contentDisposition으로 들어온 파일명을 로그로 찍어보고 앞뒤로 붙은 불필요한 텍스트 제거를 위한 코드들을 추가합니다.

( 아래 코드에서는 ";" 가 포함됐거나, "\"가 포함 된 경우 제거 합니다. ) 

        setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
            if (activity.isGrantDownloadPermission()) {
            // 파일 읽기 쓰기 권한이 허용된 경우 

                var downloadManager = context.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
                var _contentDisposition = URLDecoder.decode(contentDisposition, "UTF-8")

                var _mimetype = mimetype

				// 파일명 정제 시작 
                var fileName = _contentDisposition.replace("attachment; filename=", "")
                if (!TextUtils.isEmpty(fileName)) {
                    _mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(mimetype)

                    if (fileName.endsWith(";")) {
                        fileName = fileName.substring(0, fileName.length - 1)
                    }

                    if (fileName.startsWith("\"") && fileName.endsWith("\"")) {
                        fileName = fileName.substring(1, fileName.length - 1)
                    }
                }
                // 파일명 정제 끝

                var request = DownloadManager.Request(Uri.parse(url)).apply {
                    setMimeType(_mimetype)
                    addRequestHeader("User-Agent", userAgent)
                    setDescription("Downloading File")
                    setAllowedOverMetered(true)
                    setAllowedOverRoaming(true)
                    setTitle(fileName)
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        setRequiresCharging(false)
                    }

                    allowScanningByMediaScanner()
                    setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
                    setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
                }

                registerDownloadReceiver(downloadManager, activity)
                downloadId = downloadManager.enqueue(request)
                activity.showToast("다운로드 시작중..")
            }
        }

| 다운로드 노티 설정

다운로드 진행상황이 노티에 표시되는 모습은 안드로이드 사용 유저라면 많이 접해보셨을겁니다. DownloadManager.Request를 사용해 간편하게 설정할 수 있습니다. 

위 코드에서 'setNotificationVisibillty(int)' 를 참고하시면 되는데, 파라미터 값들로는 아래 4가지 입니다.

  • VISIBILITY_VISIBLE_NOTIFY_COMPLETED : 다운로드 진행 과정과 완료 됐을때 모두 보여줍니다.
  • VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION : 다운로드가 완료 됐을때만 보여줍니다.
  • VISIBILITY_VISIBLE : 다운로드가 진행중일 때만 보여줍니다. 
  • VISIBILITY_HIDDEN : 모두 비노출합니다.

| 다운로드 파일 바로 열기

노티 설정에 따라 다운로드가 완료됐다는 노티를 클릭한 경우 다운로드된 파일을 바로 열어볼 수 있습니다. 근데 파일 타입을 잘못 설정한 경우 '파일을 열수 없습니다.' 라는 토스트가 노출될겁니다. 다운로드 받은 파일과, 노티에 설정된 파일 타입이 일치하지 않을때 발생하는 현상입니다. 

리스너로 전달받은 'mineType' 인자를 코딩해보시면 파일과 다를거고 저 같은 경우는 application/zip으로 들어오는데 실제 파일은 엑셀인 경우였습니다.

 파일명에 있는 타입을 추출해서 하나하나 대입해줘야하나 했는데, 타입 추출하는 방법을 검색해서 적용했을땐 실제 결과값이 null 이 였고 정상적으로 작동했습니다. 

이로서 추측 해볼수 있는 상황으론, 타입이 널일 경우 파일명에서 파일 타입을 스스로 설정하는 느낌 같았습니다. 

( 제대로된 정보가 있을 경우 댓글로 남겨 주시면 반영하도록 하겠습니다. ) 

| 다운로드 완료 콜백

위 코드중 'downloadManager.enqueue'로 받은 리턴 값은 다운로드를 관리하는 아이디로, 완료됐을 경우 아이디를 비교해서 보다 정확하게 사용자에게 어떤 파일이 완료됐는지 알려줄 수 있습니다.

    private fun registerDownloadReceiver(downloadManager: DownloadManager, activity: BaseActivity<*>) {
        var downloadReceiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                var id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1

                when (intent?.action) {
                    DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {

                        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) {
                                activity.showToast("다운로드가 완료됐습니다.")
                            } else if (status == DownloadManager.STATUS_FAILED) {
                                activity.showToast("다운로드가 실패했습니다.")
                            }
                        }
                    }
                }
            }
        }

        IntentFilter().run {
            addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
            activity.registerReceiver(downloadReceiver, this)
        }
    }

 

 

위 내용에서 잘못된 내용이나 궁금하신 부분은 댓글로 남겨주시면 답변 드리도록 하겠습니다.  

감사합니다. 

 

 

 

반응형