안녕하세요
이번 글은 Android WebView에서 파일을 업로드하는 방법에 대해서 소개해보려고 합니다. 이번 주제는 Android WebView는 별도 처리 없이는 웹 사이트의 파일 업로드 기능을 사용할 수 없기 때문입니다.
문제 상황
@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 구현해 주기
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 객체는 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
'Android > WebView' 카테고리의 다른 글
[Android] Webview Alert 표시하기 (0) | 2023.10.25 |
---|---|
[Android] WebView Exception 처리하기 (0) | 2023.07.15 |
[Android] WebView에서 유투브 전체 화면을 올바르게 표시하는 방법 (0) | 2023.06.10 |