본문 바로가기

안드로이드/코틀린

Webview 이미지 업로드 구현하기 ( 카메라, 갤러리 )

반응형

웹뷰가 아닌 네이티브만 작업하시는 분들은 제 블로그에 다른 포스팅을 참고해주세요 ! 

( 그리고 해당 포스팅은 minsdk 21, Kotlin 기준으로 작업됐습니다. )

 

[안드로이드/코틀린] 카메라와 갤러리에서 이미지 가져오기

해당 포스팅은 제 블로그 조회수에 상당수를 기록했습니다. 그만큼 앱을 개발하는데 있어 이미지는 필수사항이라고 해도 과언이 아닙니다. 많이 부족함에도 불구하고 찾아주셔서 감사합니다.

superwony.tistory.com

 

| AndroidManifest.xml

권한 설정

카메라 파일을 읽고 쓰기 위해 3가지 권한을 사용합니다. 카메라로 찍은 사진을 onActivityResult로 바로 가져오지 않고, 빈 파일을 생성해서 카메라로 찍은 사진을 저장하기 때문에 저장소 권한이 필요 합니다.

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

 

저장소 target 29 대응 ( 카메라 권한 )

구글에선 앱이 디바이스 저장소에 직접 접근하는걸 29 버전부터 막기 시작했는데, 아직은 우회하는 방향을 안내하고 있는데 아래 코드를 추가하지 않으면 코드로 파일을 생성해도 작동하지 않습니다. 갤러리에서 이미지만 가져온다면 아래 코드는 필요 없습니다.

 <application
        ....
        android:requestLegacyExternalStorage="true">

 

프로 바이드 설정 ( 카메라 권한 ) 

누가(7.0) 버전부터 파일 접근시 경로가 아닌 FileProvider로 접근하도록 변경됐습니다. 

  <application
  	....>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>
	....
    </application>

meta-data resource에 명시된 provider_paths 파일은 res > xml 에 생성해야 합니다. ( xml 폴더도 처음엔 없기 때문에 생성 해야합니다. ) 

 

provider_paths.xml

'plantDiary'는 카메라로 찍은 사진을 저장할 폴더 이름이 됩니다. 

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
            name="hidden"
            path="plantDiary" />
    <external-path
            name="external_files"
            path="." />
</paths>


| WebChromeClient

웹뷰에서 파일 업로드 버튼을 클릭했을때 밑에 메소드를 호출합니다. ( 해당 코드는 minSdk 21입니다, 킷켓도 출시 범위에 포함되어 있다면 별도 대응을 해야합니다. 해당 포스팅에선 다루지 않으니 다른 포스팅을 참고해주세요! ) 

     
    val REQ_SELECT_IMAGE = 2001
    var filePathCallback: ValueCallback<Array<Uri>>? = null
     
     binding.webview.webChromeClient = object : WebChromeClient() {
            override fun onShowFileChooser(webView: WebView?, filePathCallback: ValueCallback<Array<Uri>>?, fileChooserParams: FileChooserParams?): Boolean {
                var isCapture = fileChooserParams?.isCaptureEnabled ?: false// 
                selectImage(filePathCallback)
                return true
            }
        }
        
        
    fun selectImage(filePathCallback: ValueCallback<Array<Uri>>?) {
        if (!isGrantImagePermission()) {
            filePathCallback?.onReceiveValue(null)
            return
        }

        this.filePathCallback?.onReceiveValue(null)
        this.filePathCallback = filePathCallback

        var state = Environment.getExternalStorageState()
        if (!TextUtils.equals(state, Environment.MEDIA_MOUNTED)) {
            return
        }

        var cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        cameraIntent.resolveActivity(packageManager)?.let {
            createImageFile()?.let {
                photoUri = FileProvider.getUriForFile(this@BaseActivity, BuildConfig.APPLICATION_ID + ".provider", it)
                cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
            }
        }


        var intent = Intent(Intent.ACTION_PICK).apply {
            type = MediaStore.Images.Media.CONTENT_TYPE
            data = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
        }

        Intent.createChooser(intent, "사진 가져올 방법을 선택하세요").run {
            putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(cameraIntent))
            startActivityForResult(this, REQ_SELECT_IMAGE)
        }
    }


isCaptureEnabled는 파일 업로드 버튼에 capture 속성이 있는지 판단하는 값입니다. ( true일 경우 카메라와 갤러리중 사진을 선택하도록, false일땐 갤러리에서만 선택하도록 ) 

카메라나 갤러리에서 사진을 선택했을때 filePathCallback을 통해 웹뷰에 전달해주는데, 권한등의 이슈로 적절한 값을 보내주지 못할때엔 null값을 보내 초기화를 해야 합니다. 그렇지 않으면 다시 눌렀을때 반응이 없기 때문입니다.

| 선택한 사진 웹뷰로 전달

카메라로 사진을 선택했을땐 data값이 null이기 때문에 위에서 설정한 값을 넣어서 웹뷰로 전달해야합니다. 마찬가지로 사진 값을 가져오지 못했을때도 ( resultCode가 ResultOK가 아닐때 ) callBack에 null값을 전달해줘야 합니다.


    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

            when (requestCode) {
                REQ_SELECT_IMAGE -> {
                    if (resultCode == Activity.RESULT_OK) {

                        filePathCallback?.let {

                            var imageData = data

                            if (imageData == null) {
                                imageData = Intent()
                                imageData?.data = Uri.fromFile(File(cameraImagePath))
                            }

                            if (imageData?.data == null) {
                                imageData?.data = Uri.fromFile(File(cameraImagePath))
                            }


                            it.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, imageData))
                            filePathCallback = null
                        }
                    }else{
                    
                        filePathCallback?.onReceiveValue(null)
                        filePathCallback = null
                        
                    }
                }
            }
    }

 

포스팅대로 작업했는데 문제가 발생하시는 경우 댓글로 질문해주시면 최대한 빠르게 답변 드리겠습니다.

 

감사합니다 

 

 

 

 

 

 

반응형