Android/WebView

[Android] WebView로 파일을 업로드하는 방법

점냥 2023. 9. 14. 21:02
반응형

안녕하세요

 

이번 글은 Android WebView에서 파일을 업로드하는 방법에 대해서 소개해보려고 합니다. 이번 주제는 Android WebView는 별도 처리 없이는 웹 사이트의 파일 업로드 기능을 사용할 수 없기 때문입니다.

 

문제 상황

choose File 버튼의 반응이 없다!

@SuppressLint("SetJavaScriptEnabled")
@Composable
fun UploadWebViewScreen() {
    // File Upload HTML을 사용
    val state = rememberWebViewStateWithHTMLData(
        """
            <!DOCTYPE html>
            <html>
            <body>

            <p>Click on the "Choose File" button to upload a file:</p>

            <form action="/action_page.php">
              <input type="file" id="myFile" name="filename">
              <input type="submit">
            </form>

            </body>
            </html>
        """.trimIndent()
    )
    val context = LocalContext.current

    WebView(
        state = state,
        modifier = Modifier.fillMaxWidth().height(300.dp),
        onCreated = { webView ->
            webView.settings.javaScriptEnabled = true
        }
    )
}

 

 

 

 

onShowFileChooser 구현해 주기

WebChromClient.kt에 정의되어 있는 onShowFileChooser

 

onShowFileChooser 함수는 WebChromClient의 함수입니다. Android WebView는 웹 사이트에서 파일 업로드 기능을 사용할 때 onShowFileChooser 함수를 호출하는데요. 그런데 내부 구현이 비어있어서 아무런 동작을 안 하는 것이 문제의 원인이었습니다. 그래서 이 문제를 해결하기 위해 해당 함수를 오버라이드해서 내부 구현을 해야 합니다.

 

간단하게 매개변수 자료형을 살펴보면,

  • WebView: 현재 파일 업로드 기능이 사용된 WebView 객체
  • ValueCallback<Uri[]>: 업로드하려는 File의 Uri 배열 객체를 전달받는 콜백 객체입니다. 사용자가 선택한 파일의 Uri를 이 객체로 넘겨주면 웹 사이트에서 전달받을 수 있습니다
  • FileChooserParams: 현재 호출되는 파일 업로드 기능의 부가 정보를 담고 있는 객체입니다.

 

Step1. 나만의 WebChromClient 생성하기

class FileUploadWebChromeClient(
    private val onShowFilePicker: (Intent) -> Unit
): AccompanistWebChromeClient() {
    override fun onShowFileChooser(
        webView: WebView?,
        filePathCallback: ValueCallback<Array<Uri>>?,
        fileChooserParams: FileChooserParams?
    ): Boolean {
        ...
    }

    ...
}

onShowFileChooser를 오버라이드하기 위해서는 WebChromeClient를 상속해서 클래스를 정의해야겠죠. 그리고 우리가 구현해야 하는 onShowFileChooser 함수를 오버라이드 합니다.

 

Step2. FilePathCallback 값 전달

class FileUploadWebChromeClient(
    ...
): AccompanistWebChromeClient() {
    private var filePathCallback: ValueCallback<Array<Uri>>? = null

    override fun onShowFileChooser(
        webView: WebView?,
        filePathCallback: ValueCallback<Array<Uri>>?,
        fileChooserParams: FileChooserParams?
    ): Boolean {
        this.filePathCallback = filePathCallback
        ...
    }

    fun selectFiles(uris: Array<Uri>) {
        filePathCallback?.onReceiveValue(uris)
        filePathCallback = null
    }

    fun cancelFileChooser() {
        filePathCallback?.onReceiveValue(null)
        filePathCallback = null
    }
}

 

 

ValueCallback 클래스

ValueCallback 객체는 interface로 값을 전달받는 onReceiveValue 함수 하나만 존재합니다. onReceiveValue 함수의 매개변수로 선택한 파일의 Uri 객체를 담아 호출하면 WebView에서 고스란히 File 정보를 전달받을 수 있습니다. 

 

여기서 주의해야할 점은 선택한 파일이 없을 때도 null로 보내야 하는 점입니다.

 

Step3. 파일 선택 Picker 사용하기 

class FileUploadWebChromeClient(
    private val onShowFilePicker: (Intent) -> Unit
): AccompanistWebChromeClient() {
    ...

    override fun onShowFileChooser(
        webView: WebView?,
        filePathCallback: ValueCallback<Array<Uri>>?,
        fileChooserParams: FileChooserParams?
    ): Boolean {
        ...
        val filePickerIntent = fileChooserParams?.createIntent()
        if (filePickerIntent == null) {
            cancelFileChooser()
        } else {
            onShowFilePicker(filePickerIntent)
        }
        return true
    }
}

FileChooserParams 객체는 기본적으로 웹에서 넘겨준 FileChooser의 정보를 담고 있는데요. 예를 들어, 이미지만 선택하고 싶다거나, 단일 선택 혹은 멀티 선택 등의 File Chooser 설정 정보를 가지고 있습니다. 

 

그리고 이 정보를 토대로 파일 선택할 수 있는 Activity의 Intent를 반환해주는 createIntent 함수도 제공해 주는데요. 그래서 보다 쉽게 파일 선택기 Activity를 실행할 수 있습니다.

 

 

 

전체 코드는 다음과 같습니다.

@SuppressLint("SetJavaScriptEnabled")
@Composable
fun UploadWebViewScreen() {
    val state = rememberWebViewStateWithHTMLData(
        """
            <!DOCTYPE html>
            <html>
            <body>

            <p>Click on the "Choose File" button to upload a file:</p>

            <form action="/action_page.php">
              <input type="file" id="myFile" name="filename">
              <input type="submit">
            </form>

            </body>
            </html>
        """.trimIndent()
    )

    var fileChooserIntent by remember { mutableStateOf<Intent?>(null) }

    val webViewChromeClient = remember { FileUploadWebChromeClient(
        onShowFilePicker = {
            fileChooserIntent = it
        }
    ) }

    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult(),
    ) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            val data = result.data?.data
            if (data != null) {
                // 현재 한개의 데이터만 선택되게 했지만, 멀티 선택인 경우를 고려해도 좋을 것 같아요
                webViewChromeClient.selectFiles(arrayOf(data))
            } else {
                webViewChromeClient.cancelFileChooser()
            }
        } else {
            webViewChromeClient.cancelFileChooser()
        }
    }

    LaunchedEffect(key1 = fileChooserIntent) {
        if (fileChooserIntent != null) {
             try {
                launcher.launch(fileChooserIntent)   
            } catch (e: ActivityNotFoundException) {
                // 기기에 알맞는 File picker가 없을 경우 취소  
                webViewChromeClient.cancelFileChooser()
            }
        }
    }


    WebView(
        state = state,
        modifier = Modifier
            .fillMaxWidth()
            .height(300.dp),
        onCreated = { webView ->
            webView.settings.javaScriptEnabled = true
        },
        chromeClient = webViewChromeClient
    )
}


class FileUploadWebChromeClient(
    private val onShowFilePicker: (Intent) -> Unit
): AccompanistWebChromeClient() {
    private var filePathCallback: ValueCallback<Array<Uri>>? = null

    override fun onShowFileChooser(
        webView: WebView?,
        filePathCallback: ValueCallback<Array<Uri>>?,
        fileChooserParams: FileChooserParams?
    ): Boolean {
        this.filePathCallback = filePathCallback
        val filePickerIntent = fileChooserParams?.createIntent()
        if (filePickerIntent == null) {
            cancelFileChooser()
        } else {
            onShowFilePicker(filePickerIntent)
        }
        return true
    }

    fun selectFiles(uris: Array<Uri>) {
        filePathCallback?.onReceiveValue(uris)
        filePathCallback = null
    }

    fun cancelFileChooser() {
        filePathCallback?.onReceiveValue(null)
        filePathCallback = null
    }
}

 

 

결과 화면

 

정리하면

WebView의 파일 선택 기능을 사용하려면 WebChomeClient의 onShowFileChooser 함수를 오버라이드해서 내부 구현을 해야합니다. 그리고 ValueCallback 객체로 선택한 파일의 Uri을 정보를 넘기면 됩니다!

 

이 포스티에서 사용된 코드는 아래 링크에 연결된 깃허브 저장소에서 확인할 수 있습니다.
https://github.com/jaeryo2357/posting_android_sample_code/pull/4

반응형