오늘은 저번에 배포했던 Android WebView에서 파일 업로드 기능을 구현하는 과정을 공유하려고 한다.
정말 기분이 좋게도 레거시 버전의 코드를 이전 개발자가 주고 튀었다. 그래서 기능이 비는 부분이 있어 이렇게 구현해봤다.
Android WebView에서 파일 업로드 기능을 구현하려면 중요한 단계를 거쳐야 한다. 권한 설정과 기존 Webview 설정을 건들기도 해야한다.
파일 업로드를 하려면 필요한 권한을 AndroidManifest.xml
파일에 추가해야 한다. WRITE_EXTERNAL_STORAGE
권한은 구형 Android 버전에서만 필요했기 때문에 READ_EXTERNAL_STORAGE
권한만 추가한다.
user 아니다. uses다. 내가 헷갈려서 말하는 건 아니다.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ 사진을 보면 알겠지만 READ_EXTERNAL_STORAGE
권한은 Deprecated 됐다. 요즘 Android13+은 READ_MEDIA_IMAGES
, READ_MEDIA_VIDEO
, READ_MEDIA_AUDIO
이렇게 권한을 부여하니 알아두길 바란다. 나는 Android 10 버전 이하도 지원해야하기 때문에 이렇게 하긴 했다.
MainActivity
설정이제 WebView
를 설정한다. WebChromeClient
를 설정하여 파일 선택을 처리할 수 있도록 한다. 애초에 웹뷰앱이 나오도록 MainActivity
를 구성했다면 URL을 포함한 MainActivity
코드가 있을 것이다. 나는 내 기존 코드에서 출발해서 파일 업로드 관련 코드만 올리겠다.
그 외 mWebView
, onBackPressed
, onCreate
같은 클래스, 메소드 등등은 제외 하겠다. 흔하긴 하지만 아무래도 현업에 사용하는 코드다 보니.. onBackPressed
같은 구식도 있어서 현재 쓰기엔 적합하지 않는 코드도 있어서 추출해 보여주겠다.
public class MainActivity extends Activity {
// 클래스 변수
// onCreate 메소드
...
// 1. MyWebChromeClient 클래스
private class MyWebChromeClient extends WebChromeClient {
...
@Override
public boolean onShowFileChooser(WebView webView,
ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
System.out.println("WebViewActivity A>5, OS Version : " + Build.VERSION.SDK_INT + "\t onSFC(WV,VCUB,FCP), n=3");
if (mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(null);
}
mFilePathCallback = filePathCallback;
return true;
}
}
보는 것처럼 onShowFileChooser
메소드만 있고, onActivityResult
은 다른 내용만 있지 파일 관련 코드가 없었다. 심지어 onShowFileChooser
메소드는 만들다 말았다! 이러니까 실행이 안되고 앱이 꺼지지!!
여기를 채워줘야했었다.
onShowFileChooser
메소드 (MyWebChromeClient
)이 메소드는 파일 선택기(intent)를 호출하여 사용자가 파일을 선택할 수 있도록 한다. <input type="file">
태그가 클릭됐을 때 호출 된다!
과정은 아래와 같다.
mFilePathCallback
이 이미 설정됐다면 이를 null
로 설정해 기존 파일 선택 콜백을 초기화한다. 이렇게 하면 이전 파일 선택 작업이 취소된다. 이 코드를 작성해 혹여나 생길 파일 겹침 문제를 예방한다.if (mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(null);
}
mFilePathCallback
변수에 onShowFileChooser
파라미터 중 하나인 filePathCallback
를 할당한다.mFilePathCallback = filePathCallback;
FileChooserParams
를 사용해 파일 선택기 인텐트를 생성한다. 그 후 startActivityForResult
메소드를 호출해 파일 선택기를 실행합니다.Intent intent = fileChooserParams.createIntent();
try {
startActivityForResult(intent, INPUT_FILE_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
mFilePathCallback = null;
Toast.makeText(MainActivity.this, "Cannot open file chooser", Toast.LENGTH_LONG).show();
return false;
}
return true;
잘 끝냈다면 안드로이드에 내장된 파일 호출기가 실행될 것이다.
private class MyWebChromeClient extends WebChromeClient {
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
if (mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(null);
}
mFilePathCallback = filePathCallback;
Intent intent = fileChooserParams.createIntent();
try {
startActivityForResult(intent, INPUT_FILE_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
mFilePathCallback = null;
Toast.makeText(MainActivity.this, "Cannot open file chooser", Toast.LENGTH_LONG).show();
return false;
}
return true;
}
}
onActivityResult
메소드이 메소드는 사용자가 파일 선택을 완료 / 취소 후 호출된다.
알아둘 것이 나는 구버전의 Android 버전도 지원했어야하기 때문에 예외 처리 및 취소 시의 처리를 다양하게 접목 시켰다. 최신 버전만 지원하게 작성한다면 더 간단히 할 수 있다.
예외 처리 다 빼고 일단 핵심만 작성하겠다.
1. 메소드 시작 / 부모 클래스 메소드 호출
super.onActivityResult(requestCode, resultCode, data);
는 부모 클래스의 onActivityResult
메소드를 호출해 기본 동작을 수행한다.
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
requestCode
가 파일 선택 요청 코드(INPUT_FILE_REQUEST_CODE
)와 일치하는지 확인한다. 이 코드는 파일 선택 호출기를 열 때 사용한 코드와 일치해야 한다. 예외 처리를 위한 조건이다. // 파일 선택을 위한 요청 코드 확인
if (requestCode == INPUT_FILE_REQUEST_CODE) {
resultCode
가 Activity.RESULT_OK
가 아닌 경우다. 즉 사용자가 파일 선택을 취소했거나 오류가 발생한 경우 실행하는 이중 조건문이다.mFilePathCallback
이 null
이 아니면 null
값을 전달해 파일 선택이 취소됐다는 걸 콜백에 알린다.mUploadMessage
가 null
이 아니면 null
값을 전달해 파일 선택이 취소됐다는 걸 알린다.TempimageFile
이 존재하고 삭제할 수 있다면 삭제한다.return
). // 사용자가 파일 선택을 취소했거나 결과가 OK가 아닌 경우
if (resultCode != Activity.RESULT_OK) {
if (mFilePathCallback != null) {
// 파일 선택이 취소됐음을 콜백에 전달
mFilePathCallback.onReceiveValue(null);
mFilePathCallback = null;
}
if (mUploadMessage != null) {
mUploadMessage.onReceiveValue(null);
mUploadMessage = null;
}
if (TempimageFile != null && TempimageFile.exists()) {
// 임시 파일 삭제
TempimageFile.delete();
}
return; // 메서드 종료
}
results
배열을 초기화.data
가 null
이거나 resultCode
가 RESULT_OK
가 아니라면?mCameraPhotoPath
가 null
이 아니면 임시 파일의 경로를 Uri
로 변환해 results
배열에 저장.data
가 null
이 아니라면?data.getDataString()
을 호출해 단일 파일 선택을 처리.dataString
이 null
이 아니라면, Uri.parse(dataString)
을 사용하여 Uri
객체를 생성하고 results
배열에 저장.dataString
이 null
인 경우, 다중 파일 선택을 처리.data.getClipData()
가 null
이 아닌 경우, 선택된 파일의 수(numSelectedFiles
)를 가져오고, 각 파일의 Uri
를 results
배열에 저장! Uri[] results = null;
// 파일을 성공적으로 선택한 경우
try {
if (data == null || resultCode != RESULT_OK) {
// 데이터가 없거나 결과가 OK가 아닌 경우, 임시 파일 사용
if (mCameraPhotoPath != null) {
results = new Uri[]{Uri.parse(mCameraPhotoPath)};
}
} else {
String dataString = data.getDataString();
if (dataString != null) {
// 단일 파일 선택 처리
results = new Uri[]{Uri.parse(dataString)};
} else {
// 다중 파일 선택 처리
if (data.getClipData() != null) {
final int numSelectedFiles = data.getClipData().getItemCount();
results = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
results[i] = data.getClipData().getItemAt(i).getUri();
}
}
}
}
} catch (Exception e) {
// 예외 처리 로그
Log.e("MainActivity", "File select error", e);
}
mFilePathCallback
이 null
이 아닌 경우, results
배열을 전달하여 콜백을 호출.mFilePathCallback
을 null
로 설정.mFilePathCallback
이 null
이고, mUploadMessage
가 null
이 아니라면?results
배열의 첫 번째 Uri
를 전달해 콜백을 호출.mUploadMessage
를 null
로 설정. // 결과가 null이 아니면 콜백에 Uri 배열 전달
if (mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(results);
mFilePathCallback = null;
} else if (mUploadMessage != null) {
// 구버전 API를 위한 처리
mUploadMessage.onReceiveValue(results[0]);
mUploadMessage = null;
}
}
}
+ mUploadMessage
는 구버전 안드로이드(4.4 이전)에서 파일 선택 콜백을 처리하기 위한 변수다. 누가 4.4를 쓰냐고 묻는다면 할 말 없지만 일단 해놨다.
이렇게 onActivityResult
메소드는 파일 선택 호출기에서 반환 결과를 처리 후, 선택된 파일의 경로를 콜백을 통해 전달하는 역할을 한다.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// 파일 선택을 위한 요청 코드 확인
if (requestCode == INPUT_FILE_REQUEST_CODE) {
// 사용자가 파일 선택을 취소했거나 결과가 OK가 아닌 경우
if (resultCode != Activity.RESULT_OK) {
if (mFilePathCallback != null) {
// 파일 선택이 취소됐음을 콜백에 전달
mFilePathCallback.onReceiveValue(null);
mFilePathCallback = null;
}
if (mUploadMessage != null) {
mUploadMessage.onReceiveValue(null);
mUploadMessage = null;
}
if (TempimageFile != null && TempimageFile.exists()) {
// 임시 파일 삭제
TempimageFile.delete();
}
return; // 메서드 종료
}
Uri[] results = null;
// 파일을 성공적으로 선택한 경우
try {
if (data == null || resultCode != RESULT_OK) {
// 데이터가 없거나 결과가 OK가 아닌 경우, 임시 파일 사용
if (mCameraPhotoPath != null) {
results = new Uri[]{Uri.parse(mCameraPhotoPath)};
}
} else {
String dataString = data.getDataString();
if (dataString != null) {
// 단일 파일 선택 처리
results = new Uri[]{Uri.parse(dataString)};
} else {
// 다중 파일 선택 처리
if (data.getClipData() != null) {
final int numSelectedFiles = data.getClipData().getItemCount();
results = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
results[i] = data.getClipData().getItemAt(i).getUri();
}
}
}
}
} catch (Exception e) {
// 예외 처리 로그
Log.e("MainActivity", "File select error", e);
}
// 결과가 null이 아니면 콜백에 Uri 배열 전달
if (mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(results);
mFilePathCallback = null;
} else if (mUploadMessage != null) {
// 구버전 API를 위한 처리
mUploadMessage.onReceiveValue(results[0]);
mUploadMessage = null;
}
}
}
나는 이렇게 하니 너무너무 잘됐다! 그 카타르시스가 아직도 생각이 난다.
이렇게 보니 너무 못생기긴 했는데.. 개발할 당시 너무 에러가 나서 이것 저것 붙이다 보니 이렇게 된 거 같다. 뭐 GPT도 그럴 수 있다 했고, 필요한 조건문이라 어쩔 수 없다. 나중에 메소드 분리 리팩토링을 해야할 거 같다.
이렇게 해서 Android WebView에서 파일 업로드 기능을 구현했다. 이번 도전을 통해 파일 업로드 기능을 구현할 수 있었다. 다양한 에러 상황을 배제하는 것도^^,, 이걸 읽는 독자도 도움이 됐길 빈다.