하이브리드 앱 파일 업로드 - haibeulideu aeb pail eoblodeu

그랬습니다.

안드로이드 하이브리드앱 개발할때 웹뷰에 불려올 웹페이지에서 input type="file" 넣으면

그냥 업로드 기능이 될줄로 알고 있었습니다.

하지만 그게 아니였죠.

웹뷰를 활용하여 안드로이드 하이브리드앱을 개발할때 업로드 기능을 구현하기 위해서는

추가적인 코드가 필요합니다.

그렇지 않으면 input type="file" 입력컨트롤을 아무리 터치해도 파일선택창이 나타나지 않습니다.

        WebView wv = (WebView) findViewById(R.id.webview);
        wv.setWebChromeClient(new WebChromeClient() {
            // For Android < 3.0
            public void openFileChooser( ValueCallback<uri> uploadMsg) {
                Log.d("MainActivity", "3.0 <");
                openFileChooser(uploadMsg, "");
            }
            // For Android 3.0+
            public void openFileChooser( ValueCallback<uri> uploadMsg, String acceptType) {
                Log.d("MainActivity", "3.0+");
                filePathCallbackNormal = uploadMsg;
                Intent i = new Intent(Intent.ACTION_GET_CONTENT);
                i.addCategory(Intent.CATEGORY_OPENABLE);
                i.setType("image/*");
                startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_NORMAL_REQ_CODE);
            }
            // For Android 4.1+
            public void openFileChooser(ValueCallback<uri> uploadMsg, String acceptType, String capture) {
                Log.d("MainActivity", "4.1+");
                openFileChooser(uploadMsg, acceptType);
            }

            // For Android 5.0+
            public boolean onShowFileChooser(
                    WebView webView, ValueCallback<uri[]> filePathCallback,
                    WebChromeClient.FileChooserParams fileChooserParams) {
                Log.d("MainActivity", "5.0+");
                if (filePathCallbackLollipop != null) {
                    filePathCallbackLollipop.onReceiveValue(null);
                    filePathCallbackLollipop = null;
                }
                filePathCallbackLollipop = filePathCallback;
                Intent i = new Intent(Intent.ACTION_GET_CONTENT);
                i.addCategory(Intent.CATEGORY_OPENABLE);
                i.setType("image/*");
                startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_LOLLIPOP_REQ_CODE);

                return true;
            }
        });
        WebSettings set = wv.getSettings();
        set.setJavaScriptEnabled(true);
        wv.loadUrl("http://test.com/upload.php");

웹뷰에 필요한 코드는 이렇습니다.

각 OS버전별로 override 해야할 함수가 다릅니다.

안드로이드에 openFileChooser와 onShowFileChooser라는 숨겨진 함수가 있는거죠.

해당 메소드를 재정의하고 intent를 호출하여 파일을 선택할수 있도록 해야합니다.

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == FILECHOOSER_NORMAL_REQ_CODE) {
            if (filePathCallbackNormal == null) return ;
            Uri result = (data == null || resultCode != RESULT_OK) ? null : data.getData();
            filePathCallbackNormal.onReceiveValue(result);
            filePathCallbackNormal = null;
        } else if (requestCode == FILECHOOSER_LOLLIPOP_REQ_CODE) {
            if (filePathCallbackLollipop == null) return ;
            filePathCallbackLollipop.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
            filePathCallbackLollipop = null;
        }
    }

onAcitivityResult 에서 선택한 파일을 input 컨트롤로 전달하기 위한 코드를 추가하면 끝납니다.

필요한 퍼미션은 internet만 있으면 되는데

5.0 미만 버전의 호환을 위해서 read_external_strage도 있어야 합니다.

그렇지 않으면 파일 선택후 업로드시 오류가 발생합니다.

<uses-permission android:name="android.permission.INTERNET" />

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

참고하셔야할 것은

업로드한 파일이 mime 타입이 버전별로 차이가 있습니다.

안드로이드 4.x 버전에서는 첨부한 파일의 mime가 무조건 application/octet-stream 으로 전송되니

서버측에서 다시한번 파일 검증을 하셔야 합니다.

샘플 프로젝트는 AndroidOpenFileChooser 깃헙에 공개되어있으니 여기서 확인하셔도 됩니다.

이렇게 하이브리드앱에서 업로드 기능 구현을 마쳤나 했더니 아주아주 무시무시한 일이 남아있었습니다...

그건 바로 4.4.2버전에서만 유일하게 이 코드는 동작하지 않다는것이지요...

구글링을 열심히 해보니 4.4.2버전에서는 유일하게 filechooser를 열기 위한 내장메소드가 포함되어있지 않다고 하는군요... 털썩...

4.4.2 보다 높은 버전 대상으로 배포하는 앱인 경우에는 상관이 없겠으나

그렇지 않은 경우 4.4.2 버전에 대한 예외처리를 생각해주어야 합니다.

대표적인 4.4.2 기기가 무엇이 있냐면요...

바로 갤럭시노트2...

으악 갤럭시노트2!!! 없애버리겠다!!!

이에 대처하는 코드는 다음 포스팅으로 이어집니다...

Things take time

Android(기능)

[Android] Webview의 파일 업로드(input type = 'file')

겸손할 겸 2018. 12. 6. 11:37

[파일 업로드]

기존 하이브리드앱에서는 각 위지윅에 달려있던 버튼을 커스터마이징해서 앨범열기, 카메라 찍기 뭐 이런식으로 바로 네이티브 함수를 호출했다. 그렇기 때문에 각 기능 마다 네이티브 함수를 호출하고, 끝나면 업로드함수까지 직접 호출하여 위지윅의 JS에서 해당 데이터를 받아 서버에 저장, 에디터에 Insert까지 수행했다.

그러나 지금 필요한건 이런 각 기능이 아니라 파일 탐색기를 열고, 해당 파일을 선택하면 업로드하는.. 포괄적인 기능으로 변경을 요청했다.

[로직]

예제로 간단히 해본결과, 오히려 기존에 사용하던 앨범, 카메라같이 귀찮음이 사라졌다. 특히 업로드 부분에서는 내가 JS함수를 호출할 필요 없이 완료되었어요! 라고 웹뷰에 알려주면 웹에서 자동적으로 알아서 해준다. 즉, 웹의 input type을 file로 했을 때 사용자는 해당 파일을 선택만하면 그 이후 작업은 Html에서 해야하는 것처럼, 이후 작업 자체가 앱에서 할 일이 없어졌다는 것이다.

    ValueCallback mFilePathCallback;

해당 클래스내 전역변수를 하나 선언한다. 이 변수는 웹뷰의 특정 함수를 override할 때 사용할 변수이다.

        webView.setWebChromeClient(new WebChromeClient(){
            @Override
            public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) {
                mFilePathCallback = filePathCallback;

                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("image/*");

                startActivityForResult(intent, 0);
                return true;
            }
        });

웹뷰를 받아서 WebChromeClient를 상속받는다. 이 클래스는 Gps, Confirm, Alert 등의 작업이 웹에서 호출되었을 때 네이티브에서 어떻게 할 것인지에 대해 정의하는 클래스므로, 위의 onShowFileChooser만 사용하는 것이 아님이다.

간단히 보자면, input type = 'file'이 걸린 버튼을 클릭했을 때 호출되는 함수이다. 그래서 mFilePathCallback 변수가 여기서 직접 대입이 되고 intent를 통해 Content타입이면서 image타입인 것을 보여달라가 되는 것이다.

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        Log.e("resultCode:: ", String.valueOf(resultCode));
        if(requestCode == 0 && resultCode == Activity.RESULT_OK){
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                mFilePathCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
            }else{
                mFilePathCallback.onReceiveValue(new Uri[]{data.getData()});
            }
            mFilePathCallback = null;
        }else{
            mFilePathCallback.onReceiveValue(null);
        }
    }

간단한 소스코드. 이 방법대로 하게 되면, 기본적인 이미지 파일 선택 및 업로드가 가능하다.

[추가]

여기서 나는 파일 업로드 뿐 아니라 사진 촬영까지 같이 하고 싶다면, 카메라 찍는 권한부터 파일 생성까지 하고 그 이후에 onShowFileChooser에서 intent에 ImageCapture 인텐트를 넣으면 된다. 기회가 된다면, 추가할 예정이나 현재 필요없다고 하니까 뭐..