[컴][안드로이드] android web app 에서 pdf download 기능 구현 방법

 웹앱 다운로드 /웹앱에서 다운로드 구현 / 다운로드 매니저/ download manager in web app webapp / android webapp

android web app 에서 pdf download 기능

만약 pdf 가 public link 로 되어 있다면 간단하다. webView 에 download listener 를 등록해주면 된다.


// MainActivity.kt

import android.Manifest
import android.app.Activity
import android.app.DownloadManager
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.webkit.*
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import com.mycomp.R


class MainActivity : Activity() {
    val FILECHOOSER_REQ_CODE = 2001

    private val webView by lazy { findViewById<WebView>(R.id.webview) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        ...
        bindViews()
        ...
    }


    var valueCallback: ValueCallback<Array<Uri>>? = null
    private fun bindViews() {
        with(webView) {

            webViewClient = MyWebClient(this@MainActivity, deepLink)
            webChromeClient = object : WebChromeClient() {
                override fun onShowFileChooser(
                    webView: WebView?,
                    filePathCallback: ValueCallback<Array<Uri>>?,
                    fileChooserParams: FileChooserParams?
                ): Boolean {
                    if (valueCallback != null) {
                        valueCallback?.onReceiveValue(null)
                        valueCallback = null
                    }
                    valueCallback = filePathCallback

                    checkPermission()
                    return true
                }
            }
            settings.textZoom = 100
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true

            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
                val cookieManager = CookieManager.getInstance()
                cookieManager.setAcceptCookie(true)
                cookieManager.setAcceptThirdPartyCookies(this@with, true)
            }

            // ------------------------------------------------------------------------
            //
            // download listener
            //
            // ------------------------------------------------------------------------
            this@with.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
                //checking Runtime permissions
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
                        //Do this, if permission granted
                        downloadDialog(url, userAgent, contentDisposition, mimetype)
                    } else {
                        //Do this, if there is no permission
                        ActivityCompat.requestPermissions(
                                this@MainActivity,
                                arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                                1
                        )
                    }
                } else {
                    //Code for devices below API 23 or Marshmallow
                    downloadDialog(url, userAgent, contentDisposition, mimetype)
                }
            }
        }

    }

    fun downloadDialog(url: String, userAgent: String, contentDisposition: String, mimetype: String) {
        //getting file name from url
        val filename = getFileName(url, contentDisposition, mimetype)
        
        //Alertdialog
        val builder = AlertDialog.Builder(this)
        //title for AlertDialog
        builder.setTitle("Download")
        //message of AlertDialog
        builder.setMessage("Do you want to save $filename")
        //if YES button clicks
        builder.setPositiveButton("Yes") { dialog, which ->
            val urltoken = url.split("$$")
            //DownloadManager.Request created with url.
            val request = DownloadManager.Request(Uri.parse(urltoken[0]))
            //cookie
            val cookie = CookieManager.getInstance().getCookie(url)
            //Add cookie and User-Agent to request
            request.addRequestHeader("Cookie", cookie)
            request.addRequestHeader("User-Agent", userAgent)
            request.addRequestHeader("Authorization", "Bearer " + urltoken[1])

            //file scanned by MediaScannar
            request.allowScanningByMediaScanner()
            //Download is visible and its progress, after completion too.
            request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
            //DownloadManager created
            val downloadmanager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
            //Saving file in Download folder
            request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
            //download enqued
            downloadmanager.enqueue(request)
        }
        //If Cancel button clicks
        builder.setNegativeButton("Cancel")
        { dialog, which ->
            //cancel the dialog if Cancel clicks
            dialog.cancel()
        }
        val dialog: AlertDialog = builder.create()
        //alertdialog shows
        dialog.show()
    }

    private fun getFileName(url: String, contentDisposition: String, mimetype: String): String {
        var filename = URLUtil.guessFileName(url, contentDisposition, mimetype)

        // attachment; filename=_20210102_175545.xlsx; filename*=utf-8''%EC%82%AC%EC%9A%A9%EC%9E%90%EB%AA%A9%EB%A1%9D_20210102_175545.xlsx
        // to handle the "filename*=..."
        val regex = Regex("filename\\*=.*\\'\\'(.*)$", setOf(RegexOption.DOT_MATCHES_ALL))
        val matchResult = regex.find(filename)
        if(matchResult != null && matchResult.groups[1] != null){
            filename = matchResult.groups[1]?.value ?: filename;
        }
        return filename
    }

    ...
}

만약 auth 가 필요하다면 token 등을 GET parameter 로 넘기면 위의 publick link 를 사용하는 방법으로 처리할 수 있다.

그것이 아니라 만약 header 에 token 을 넣어야 하는 경우라면, app 이 token 을 가져올 방법이 있어야 한다. 또는 이것을 그냥 Blob 으로 받고, 이 Blob 으로 받은 것을 다시 저장하는 방법도 있다.

  • GET parameter 로 token 을 전달
  • Auth Header 에 넣는 경우
    • js 에서 blob download 로 처리되게 하고, blob 을 base64 로 변환해서 다시 저장하는 방법
    • token 을 app 으로 전달해서 header 에 token 을 채우는 방법

Custom URI schema

“token 을 app 으로 전달해서 header 에 token 을 채우는 방법” 을 사용했는데, 이를 위해서 URI scheme 을 설정했다. scheme 은 아래의 모양을 띤다.

mycomp://download?url=<pdf-url>&token=<mytoken>&param1=paramval1

그리고 이 URI 를 click 할때의 action 에 대해서 override 를 했다. WebViewClient.shouldOverrideUrlLoading 을 override 하면 된다.


override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
    val intent = parse(url ?: "")
    return when {
        ...
        isMyCompURI(url) -> {
            if(url != null) {
                val (downloadUrl, token) = _parseURI(url)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    if (checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
                        //Do this, if permission granted
                        createAndShowDownloadDialog(view, downloadUrl ?: "", token);
                    } else {
                        //Do this, if there is no permission
                        ActivityCompat.requestPermissions(
                                activity,
                                arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                                1
                        )
                    }
                } else {
                    //Code for devices below API 23 or Marshmallow
                    createAndShowDownloadDialog(view, downloadUrl ?: "", token);
                }
                
            }
            return true
        }
        else -> !(url?.startsWith("http://") == true || url?.startsWith("https://") == true)
    }
}

private fun isMyCompURI(url: String?) = url?.matches(Regex("^mycomp://\\S+$")) ?: false

JavaScriptInterface

“js 에서 blob download 로 처리되게 하고, blob 을 base64 로 변환해서 다시 저장하는 방법” 은 아래 링크를 참고하자.

See Also

  1. laravel 에서 pdf download

댓글 없음:

댓글 쓰기