안드로이드 웹뷰 기능 제작중

package com.sihwawon.android

import android.R.attr
import android.R.attr.*
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Message
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.webkit.*
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import java.io.File
import java.util.*
import java.util.UUID
import android.widget.Toast
import android.view.Gravity
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import android.webkit.ValueCallback
import java.io.IOException
import java.text.SimpleDateFormat
import android.app.Activity
import android.content.Intent.ACTION_VIEW
import android.content.res.Configuration
import android.text.TextUtils
import android.webkit.WebChromeClient
import android.os.Parcelable
import androidx.core.content.FileProvider
import android.webkit.WebChromeClient.FileChooserParams
import androidx.annotation.RequiresApi
import android.print.PrintAttributes
import android.print.PrintManager
import android.view.KeyEvent
import kotlin.time.days


class MainActivity : AppCompatActivity() {

    private lateinit var mWebView: WebView
    private var pWebView: WebView? = null
    private lateinit var mProgressBar: ProgressBar
    private var thisUrl: String? = null
    private var barcode: String? = ""
    private var popup: Boolean? = false

    //for attach files
    private var mCameraPhotoPath: String? = null
    private var mFilePathCallback: ValueCallback<Array<Uri>>? = null
    internal var doubleBackToExitPressedOnce = false
    val INPUT_FILE_REQUEST_CODE = 1


    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 프로그램 시작
        var version = BuildConfig.VERSION_CODE
        // dbHelper = DBHelper(this, "local.db", null, version.toInt())
        // database = dbHelper.writableDatabase




        mWebView = findViewById(R.id.myWebView)
        pWebView = findViewById(R.id.ptWebView)
        mProgressBar = findViewById(R.id.progressBar)

        mWebView.apply {
            webViewClient = WebViewClientClass()  // new WebViewClient()); //클릭시 새창 안뜨게

            //팝업이나 파일 업로드 등 설정해주기 위해 webView.webChromeClient를 설정
            //웹뷰에서 크롬이 실행가능&& 새창띄우기는 안됨
            //webChromeClient = WebChromeClient()

            //웹뷰에서 팝업창 호출하기 위해
            webChromeClient = object : WebChromeClient() {
                @SuppressLint("SetJavaScriptEnabled")
                override fun onCreateWindow(view: WebView?, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message?): Boolean {
                    val newWebView = WebView(this@MainActivity).apply {
                        webViewClient = WebViewClientClass()
                        settings.javaScriptEnabled = true
                        settings.javaScriptCanOpenWindowsAutomatically = true
                        settings.setSupportMultipleWindows(true)
                        settings.setAllowFileAccess(true)
                    }

                    val dialog = Dialog(this@MainActivity).apply {
                        setContentView(newWebView)
                        window!!.attributes.width = ViewGroup.LayoutParams.MATCH_PARENT
                        window!!.attributes.height = ViewGroup.LayoutParams.MATCH_PARENT
                        show()
                    }

                    newWebView.webChromeClient = object : WebChromeClient() {
                        // 경고창 제어
                        override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
                            AlertDialog.Builder(view.context)
                                .setTitle("안내")
                                .setMessage(message)
                                .setPositiveButton(
                                    android.R.string.ok
                                ) { dialog, which -> result.confirm() }
                                .setCancelable(false)
                                .create()
                                .show()
                            return true
                        }

                        // 확인창 제어
                        override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
                            AlertDialog.Builder(view!!.context)
                                .setTitle("확인")
                                .setMessage(message)
                                .setPositiveButton(
                                    "Yes"
                                ) { dialog, which -> result!!.confirm() }
                                .setNegativeButton(
                                    "No"
                                ) { dialog, which -> result!!.cancel() }
                                .setCancelable(false)
                                .create()
                                .show()
                            return true
                        }

                        // 입력창 제어
                        override fun onJsPrompt(view: WebView?, url: String?, message: String?, defaultValue: String?, result: JsPromptResult?): Boolean {
                            return super.onJsPrompt(view, url, message, defaultValue, result)
                        }
                        //
                        override fun onShowFileChooser(
                            webView: WebView?,
                            filePathCallback: ValueCallback<Array<Uri>>?,
                            fileChooserParams: FileChooserParams?
                        ): Boolean {
                            if (mFilePathCallback != null) {
                                mFilePathCallback!!.onReceiveValue(null)
                            }
                            mFilePathCallback = filePathCallback

                            var takePictureIntent: Intent? = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                            if (takePictureIntent!!.resolveActivity(this@MainActivity.packageManager) != null) {
                                // Create the File where the photo should go
                                var photoFile: File? = null
                                try {
                                    photoFile = createImageFile()
                                    takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath)
                                } catch (ex: IOException) {
                                    // Error occurred while creating the File
                                    Log.e("debug::", "Unable to create Image File", ex)
                                }

                                // Continue only if the File was successfully created
                                if (photoFile != null) {
                                    mCameraPhotoPath = "file:" + photoFile.absolutePath
                                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                                        Uri.fromFile(photoFile))
                                } else {
                                    takePictureIntent = null
                                }
                            }

                            val contentSelectionIntent = Intent(Intent.ACTION_GET_CONTENT)
                            contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE)
                            contentSelectionIntent.type = "image/*"

                            // 파일 업로드 선택할 수 있게 처리함.
                            val intentArray: Array<Intent?>
                            if (takePictureIntent != null) {
                                intentArray = arrayOf(takePictureIntent)
                            } else {
                                intentArray = arrayOfNulls(0)
                            }

                            val chooserIntent = Intent(Intent.ACTION_CHOOSER)
                            chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent)
                            chooserIntent.putExtra(Intent.EXTRA_TITLE, "선택")
                            chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray)
                            startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE)

                            return true
                        }

                        //
                        override fun onCloseWindow(window: WebView?) {
                            dialog.dismiss()
                            newWebView.destroy()
                            window?.destroy()
                        }
                    }

                    //
                    dialog?.setOnDismissListener {
                        newWebView.destroy()
                    }

                    // 자바스크립트 호출 Bridge 함수
                    newWebView.addJavascriptInterface(CustomJavaScriptCallback(), "appCall");

                    //
                    (resultMsg?.obj as WebView.WebViewTransport).webView = newWebView
                    resultMsg.sendToTarget()
                    return true
                }
            }

            // 웹뷰 설정
            settings.javaScriptEnabled = true
            settings.setSupportMultipleWindows(true) // 새창띄우기 허용여부
            settings.javaScriptCanOpenWindowsAutomatically = true // 자바스크립트 새창뛰우기 (멀티
            settings.loadWithOverviewMode = true //메타태크 허용여부
            settings.useWideViewPort = true //화면 사이즈 맞추기 허용여부
            settings.setSupportZoom(true)//화면 줌 허용여부
            settings.builtInZoomControls = false//화면 확대 축소 허용여부

            // Enable and setup web view cache
            settings.cacheMode = WebSettings.LOAD_NO_CACHE //브라우저 캐시 허용여부 // WebSettings.LOAD_DEFAULT
            settings.domStorageEnabled =  true//로컬저장소 허용여부
            settings.displayZoomControls = true

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                settings.safeBrowsingEnabled = true// api 26
            }

            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                settings.mediaPlaybackRequiresUserGesture = false
            }

            settings.allowContentAccess = true

            // 아래 설정을 이용하니깐 앱 크래시가 나서 제거함.
            //settings.setGeolocationEnabled(true)
            //if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            //    settings.allowUniversalAccessFromFileURLs = true
            //}

            settings.allowFileAccess = true
            //settings.loadsImagesAutomatically = true
            fitsSystemWindows = true
            settings.setAllowFileAccess(true)
            settings.allowFileAccessFromFileURLs = true
        }
        //
        mWebView.settings.userAgentString = mWebView.getSettings().getUserAgentString() + " MySystems (trudskr_app) (uuid|"+GetDevicesUUID()+")"

        // 자바스크립트 호출 Bridge 함수
        mWebView.addJavascriptInterface(CustomJavaScriptCallback(), "appCall");

        //
        val url = "https://blog.truds.kr"
        mWebView.loadUrl(url)
        mWebView.requestFocus() // 포커스 이동하기
    }

    // URL 주소로 프린터하기
    private fun doWebViewPrint(ss: String) {
        pWebView!!.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
                return false
            }

            override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
                pWebView!!.visibility = View.INVISIBLE
            }

            override fun onPageFinished(view: WebView, url: String) {
                val printManager = getSystemService(PRINT_SERVICE) as PrintManager
                val jobName = "D-POP Printer 서비스"
                val printAdapter = pWebView!!.createPrintDocumentAdapter(jobName)
                printManager.print(jobName, printAdapter, PrintAttributes.Builder().build())
                super.onPageFinished(view, url)
            }

            override fun onPageCommitVisible (view: WebView, url: String) {
                pWebView!!.visibility = View.GONE
            }
        }
        //
        pWebView!!.loadUrl(ss)
        //pWebView!!.loadDataWithBaseURL(null, ss, "text/html", "UTF-8", null);
    }

    // UUID 획득
    private fun GetDevicesUUID() : String {
        val uuid = UUID.randomUUID()
        val randomUUIDString = uuid.toString().toUpperCase().trim()
        return randomUUIDString
    }

    // 사진을 찍으면은 이미지 파일을 작성해서 업로드 하기 위한 함수
    @Throws(IOException::class)
    private fun createImageFile(): File {
        // Create an image file name
        val timeStamp = SimpleDateFormat("yyyyMMddHHmmss").format(Date())
        val imageFileName = timeStamp
        val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)

        return File.createTempFile(
            imageFileName, /* prefix */
            ".jpg", /* suffix */
            storageDir      /* directory */
        )
    }

    //
    public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode != INPUT_FILE_REQUEST_CODE || mFilePathCallback == null) {
            super.onActivityResult(requestCode, resultCode, data)
            return
        }

        var results: Array<Uri>? = null
        // Check that the response is a good one
        if (resultCode == Activity.RESULT_OK) {
            if (data == null) {
                // If there is not data, then we may have taken a photo
                if (mCameraPhotoPath != null) {
                    results = arrayOf(Uri.parse(mCameraPhotoPath))
                }
            } else {
                val dataString = data.dataString
                if (dataString != null) {
                    results = arrayOf(Uri.parse(dataString))
                }
            }
        }

        mFilePathCallback!!.onReceiveValue(results)
        mFilePathCallback = null

        return
    }

    //웹뷰에서 홈페이지 JS 인터페이스 연결하는 Briget
    inner class CustomJavaScriptCallback {
        @JavascriptInterface
        fun print() {
            runOnUiThread { doWebViewPrint(thisUrl.toString()) }
        }
    }

    //웹뷰에서 홈페이지를 띄웠을때 새창이 아닌 기존창에서 실행이 되도록 아래 코드를 넣어준다.
    inner class WebViewClientClass: WebViewClient() {
        override fun shouldOverrideKeyEvent(view: WebView?, event: KeyEvent?): Boolean {
//            return super.shouldOverrideKeyEvent(view, event)
            val keyCode = event!!.keyCode
            val unicodeChar = event!!.unicodeChar.toChar()

            if(event.action == KeyEvent.ACTION_DOWN) {
                //
                when (keyCode) {
                    KeyEvent.KEYCODE_BACK -> {
                        mWebView.goBack()
                        return true
                    }
                }

            }
            return false
        }

        override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
            if(url.startsWith("intent://")){
                startActivity(Intent(ACTION_VIEW, Uri.parse(url.replace("intent://", "http://"))))
            } else {
                view.loadUrl(url)
            }
            return true
        }

        override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
            return super.shouldOverrideUrlLoading(view, request)
        }

        override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
            super.onPageStarted(view, url, favicon)
            mWebView.visibility = View.INVISIBLE
            mProgressBar.visibility = ProgressBar.VISIBLE
        }

        override fun onPageFinished(view: WebView?, url: String?) {
            super.onPageFinished(view, url)
            thisUrl = url.toString()
            barcode = ""    // 바코드 초기화
        }

        override fun onLoadResource(view: WebView?, url: String?) {
            super.onLoadResource(view, url)
        }

        override fun onPageCommitVisible (view: WebView, url: String) {
            super.onPageCommitVisible(view, url)
            mProgressBar.visibility = ProgressBar.GONE
            mWebView.visibility = View.VISIBLE
        }

        override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
            when (error?.errorCode) {
                ERROR_AUTHENTICATION -> { showToast("서버에서 사용자 인증 실패") }
                ERROR_BAD_URL -> { showToast("잘못된 URL") }
                ERROR_CONNECT -> { showToast("서버로 연결 실패") }
                ERROR_FAILED_SSL_HANDSHAKE -> { showToast("SSL handshake 수행 실패") }
                ERROR_FILE -> { showToast("일반 파일 오류") }
                ERROR_FILE_NOT_FOUND -> { showToast("파일을 찾을 수 없습니다") }
                ERROR_HOST_LOOKUP -> { showToast("서버 또는 프록시 호스트 이름 조회 실패")}
                ERROR_IO -> { showToast("서버에서 읽거나 서버로 쓰기 실패")}
                ERROR_PROXY_AUTHENTICATION -> { showToast("프록시에서 사용자 인증 실패")}
                ERROR_REDIRECT_LOOP -> { showToast("너무 많은 리디렉션")}
                ERROR_TIMEOUT -> { showToast("연결 시간 초과")}
                ERROR_TOO_MANY_REQUESTS -> { showToast("페이지 로드중 너무 많은 요청 발생")}
                //ERROR_UNKNOWN -> { showToast("일반 오류") }
                ERROR_UNSUPPORTED_AUTH_SCHEME -> { showToast("지원되지 않는 인증 체계")}
                ERROR_UNSUPPORTED_SCHEME -> { showToast("URI가 지원되지 않는 방식")}
            }
            super.onReceivedError(view, request, error)
        }

        override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) {
            super.onReceivedHttpError(view, request, errorResponse)
        }

        override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
            var builder: android.app.AlertDialog.Builder =
                android.app.AlertDialog.Builder(this@MainActivity)
            var message = "SSL Certificate error."
            when (error.primaryError) {
                SslError.SSL_UNTRUSTED -> message = "The certificate authority is not trusted."
                SslError.SSL_EXPIRED -> message = "The certificate has expired."
                SslError.SSL_IDMISMATCH -> message = "The certificate Hostname mismatch."
                SslError.SSL_NOTYETVALID -> message = "The certificate is not yet valid."
            }
            message += " Do you want to continue anyway?"
            builder.setTitle("SSL Certificate Error")
            builder.setMessage(message)
            builder.setPositiveButton("continue",
                DialogInterface.OnClickListener { _, _ -> handler.proceed() })
            builder.setNegativeButton("cancel",
                DialogInterface.OnClickListener { dialog, which -> handler.cancel() })
            val dialog: android.app.AlertDialog? = builder.create()
            dialog?.show()
        }
    }

    // Toast 로 메세지 띄우기
    fun showToast(msg : String) {
        var toast = Toast.makeText(applicationContext, msg, Toast.LENGTH_LONG)
        toast.setGravity(Gravity.CENTER, Gravity.CENTER_HORIZONTAL, Gravity.CENTER_VERTICAL)
        toast.show()
    }

    // 전체 화면 모드로 전환
    private fun doFullScreen(){
        window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
                // Set the content to appear under the system bars so that the
                // content doesn't resize when the system bars hide and show.
                or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                // Hide the nav bar and status bar
                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                or View.SYSTEM_UI_FLAG_FULLSCREEN)
    }

    // 뒤로가기 버튼 종료전에 확인창 나오도록 처리
    override fun onBackPressed() {
        if (mWebView.canGoBack()) {
            mWebView.goBack()
        } else {
            var builder = AlertDialog.Builder(this)
                .setTitle("안내")
                .setMessage("종료 하시겠습니까?")
                .setPositiveButton("Yes") { dialog, which -> super.onBackPressed() }
                .setNeutralButton("NO", null)
                .create()
            builder.show()
        }
    }
}

별 다른 내용은 없습니다. 8월에 블로그에 포스트를 하지 않은것 같아서 현재 제작중인 APP의 소스코드를 등록합니다. 현재 웹뷰를 이용해서 웹을 앱처럼 사용하는 하이브리드 기능을 제작 합니다. 단순한 웹뷰의 기능을 넘어서서 웹과 소통도 하고 작동하도록 하기 위해서 많은 기능이 필요합니다.

조금은 안드로이드 네거티브 앱 제작을 배워야 하지만 현재는 웹이 중심에 있기에 앱 자체를 네거티브보다는 이렇게 하이브리드 기능을 통해서 제작을 해서 처리할 생각입니다. 코도를 작성하다 보니 곳곳에 흩어진 기능을 모아서 적용하고 다듬는 과정을 거치면서 하나하나 적용을 하고 있습니다.

하이브리드 앱도 마음먹고 웹과 소통을 제작하기 시작하면은 골치아픈 일이 많다는것을 느꼈습니다. 단순하게 앱안에 웹을 표현만 해주는것이라면은 편하고 간단하게 될 거라는 생각은 안일한 생각이였다는것을 느끼게 되었으며 좀 더 신중하게 접근을 해서 완벽하게 기능을 구현하려고 준비중입니다.

아직은 다운로드 기능이 없지만 다운로드 기능까지 첨부가 되면은 웹의 기능을 좀 더 많이 구현하게 될것이라고 보여집니다. 이 코드의 기능은 WEB + Android + WinForm 를 모두 소통가능하게 처리하기 위해서 3가지를 동시에 하기에 순수한 웹과 안드로이드 앱을 중심으로 처리하지는 않습니다.

앞으로는 생각은 웹을 중심에 두고 iOS, Android, Windows 이 3가지를 모두 소통가능하도록 처리할 생각이기에 코드가 조금은 다를수 있다는 생각이 있지만 아마 기본 코드로 사용되어 지지 않을가 싶습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다