Search code examples
phpandroidwebviewruntime-errorhtml2pdf

My website use html2pdf(javascript) to generate pdf from html but have issues of download on android webview: kotlin


My website use html2pdf(javascript) to generate pdf from html but have issues of download on android webview: kotlin. When I want to click download in my webview which display my website button it just crash. I was hoping from you guys who experience why it happened and how to overcome this? I'm still beginner of this android studio development.

In the website(javascript):


function generatePDF(){
    var station_element = document.getElementById('station_content');
    var inspect_element = document.getElementById('inspect_content');
    window.scrollTo(0, 0);
    // Delay execution for 500 milliseconds (adjust as needed)
    setTimeout(function() {
        if (station_element) {
            var opt = {
                margin: 50,
                filename: 'myfile.pdf',
                image: { type: 'jpeg', quality: 1 },
                html2canvas: { scale: 5, width: 1300 },
                jsPDF: { unit: 'pt', format: 'a4', orientation: 'portrait' }
            };
            html2pdf().set(opt).from(station_element).save();
        } else if (inspect_element) {
            var opt = {
                margin: 60,
                filename: 'myfile.pdf',
                image: { type: 'jpeg', quality: 1 },
                html2canvas: { scale: 2 },
                jsPDF: { unit: 'pt', format: 'a4', orientation: 'portrait' }
            };
            html2pdf().set(opt).from(inspect_element).save();
        }
    }, 500); // 500 milliseconds delay
}

Android studio: MainActivity.kt

package com.coding.meet.webviewtoapp


class MainActivity : AppCompatActivity() {
    private var webUrl = "https://smartappx.site"
    private val multiplePermissionId = 14
    private val multiplePermissionNameList =
        if (Build.VERSION.SDK_INT >= 33) {
            arrayListOf()
        } else {
            arrayListOf(
                android.Manifest.permission.READ_EXTERNAL_STORAGE,
                android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
            )
        }

    private var isLoaded = false
    private var doubleBackToExitPressedOnce = false

    private val networkConnectivityObserver: NetworkConnectivityObserver by lazy {
        NetworkConnectivityObserver(this)
    }

    private val loadingDialog: Dialog by lazy { Dialog(this) }

    private val mainBinding: ActivityMainBinding by lazy {
        DataBindingUtil.setContentView(this, R.layout.activity_main)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        loadingDialog.setContentView(R.layout.loading_layout)
        loadingDialog.window!!.setLayout(
            LinearLayout.LayoutParams.WRAP_CONTENT,
            LinearLayout.LayoutParams.WRAP_CONTENT
        )
        loadingDialog.setCancelable(false)
        loadingDialog.show()

        val setting = mainBinding.webView.settings
        setting.javaScriptEnabled = true
        setting.allowFileAccess = true
        setting.domStorageEnabled = true
        setting.javaScriptCanOpenWindowsAutomatically = true
        setting.supportMultipleWindows()

        val snackbar =
            Snackbar.make(mainBinding.root, "No Internet Connection", Snackbar.LENGTH_INDEFINITE)
                .setAction("Wifi") { startActivity(Intent(Settings.ACTION_WIFI_SETTINGS)) }

        networkConnectivityObserver.observe(this) {
            when (it) {
                Status.Available -> {
                    if (snackbar.isShown) {
                        snackbar.dismiss()
                    }
                    mainBinding.swipeRefresh.isEnabled = true
                    if (!isLoaded) loadWebView()
                }
                else -> {
                    showNoInternet()
                    snackbar.show()
                    mainBinding.swipeRefresh.isRefreshing = false
                }
            }
        }

        mainBinding.swipeRefresh.setOnRefreshListener {
            if (!isLoaded) {
                loadWebView()
            } else {
                setProgressDialogVisibility(false)
            }
        }
    }

    private fun setProgressDialogVisibility(visible: Boolean) {
        if (visible) {
            loadingDialog.show()
        } else {
            loadingDialog.dismiss()
            mainBinding.swipeRefresh.isRefreshing = false
        }
    }

    private fun showNoInternet() {
        isLoaded = false
        setProgressDialogVisibility(false)
        gone(mainBinding.webView)
        visible(mainBinding.noInternet.noInternetRL)
    }

    private fun loadWebView() {
        gone(mainBinding.noInternet.noInternetRL)
        visible(mainBinding.webView)
        mainBinding.webView.loadUrl(webUrl)
        mainBinding.webView.setDownloadListener {
            url,
            userAgent,
            contentDisposition,
            mimeType,
            contentLength ->
            Log.d("Url", url.trim())
            Log.d("userAgent", userAgent)
            Log.d("contentDisposition", contentDisposition)
            Log.d("mimeType", mimeType)
            Log.d("contentLength", contentLength.toString())
            if (checkMultiplePermission()) {
                download(url.trim(), userAgent, contentDisposition, mimeType, contentLength)
            }
        }
        mainBinding.webView.webViewClient =
            object : WebViewClient() {
                override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                    setProgressDialogVisibility(true)
                    super.onPageStarted(view, url, favicon)
                }

                override fun shouldOverrideUrlLoading(
                    view: WebView?,
                    request: WebResourceRequest?,
                ): Boolean {
                    val url = request?.url.toString()
                    view?.loadUrl(url)
                    return super.shouldOverrideUrlLoading(view, request)
                }

                override fun onPageFinished(view: WebView?, url: String?) {
                    isLoaded = true
                    webUrl = url!!
                    setProgressDialogVisibility(false)
                    super.onPageFinished(view, url)
                }

                override fun onReceivedError(
                    view: WebView?,
                    request: WebResourceRequest?,
                    error: WebResourceError?,
                ) {
                    isLoaded = false
                    setProgressDialogVisibility(false)
                    super.onReceivedError(view, request, error)
                }
            }
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        if (event.action == KeyEvent.ACTION_DOWN) {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                if (mainBinding.webView.canGoBack()) {
                    mainBinding.webView.goBack()
                } else {
                    showToastExit()
                }
                return true
            }
        }

        return super.onKeyDown(keyCode, event)
    }

    private fun showToastExit() {
        when {
            doubleBackToExitPressedOnce -> {
                finish()
            }
            else -> {
                doubleBackToExitPressedOnce = true
                Toast.makeText(this, "Please Click Back Again to Exit", Toast.LENGTH_LONG).show()
                Handler(Looper.getMainLooper())
                    .postDelayed({ doubleBackToExitPressedOnce = false }, 2000)
            }
        }
    }

    private fun download(
        url: String,
        userAgent: String,
        contentDisposition: String,
        mimeType: String,
        contentLength: Long
    ) {
        val folder = File(Environment.getExternalStorageDirectory().toString() + "/Download/Image")
        if (!folder.exists()) {
            folder.mkdirs()
        }
        Toast.makeText(this, "Download Started", Toast.LENGTH_SHORT).show()

        val request = DownloadManager.Request(Uri.parse(url))
        request.setMimeType(mimeType)
        val cookie = CookieManager.getInstance().getCookie(url)
        request.addRequestHeader("cookie", cookie)
        request.addRequestHeader("User-Agent", userAgent)
        request.setAllowedNetworkTypes(
            DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE
        )
        val fileName = URLUtil.guessFileName(url, contentDisposition, mimeType)
        request.setTitle(fileName)
        request.setNotificationVisibility(
            DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
        )
        request.setDestinationInExternalPublicDir(
            Environment.DIRECTORY_DOWNLOADS,
            "Image/$fileName"
        )
        val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
        downloadManager.enqueue(request)
    }

    private fun checkMultiplePermission(): Boolean {
        val listPermissionNeeded = arrayListOf<String>()
        for (permission in multiplePermissionNameList) {
            if (
                ContextCompat.checkSelfPermission(this, permission) !=
                    PackageManager.PERMISSION_GRANTED
            ) {
                listPermissionNeeded.add(permission)
            }
        }
        if (listPermissionNeeded.isNotEmpty()) {
            ActivityCompat.requestPermissions(
                this,
                listPermissionNeeded.toTypedArray(),
                multiplePermissionId
            )
            return false
        }
        return true
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray,
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if (requestCode == multiplePermissionId) {
            if (grantResults.isNotEmpty()) {
                var isGrant = true
                for (element in grantResults) {
                    if (element == PackageManager.PERMISSION_DENIED) {
                        isGrant = false
                    }
                }
                if (isGrant) {
                    // here all permission granted successfully
                    Toast.makeText(this, "all permission granted successfully", Toast.LENGTH_LONG)
                        .show()
                } else {
                    var someDenied = false
                    for (permission in permissions) {
                        if (
                            !ActivityCompat.shouldShowRequestPermissionRationale(this, permission)
                        ) {
                            if (
                                ActivityCompat.checkSelfPermission(this, permission) ==
                                    PackageManager.PERMISSION_DENIED
                            ) {
                                someDenied = true
                            }
                        }
                    }
                    if (someDenied) {
                        // here app Setting open because all permission is not granted
                        // and permanent denied
                        appSettingOpen(this)
                    } else {
                        // here warning permission show
                        warningPermissionDialog(this) { _: DialogInterface, which: Int ->
                            when (which) {
                                DialogInterface.BUTTON_POSITIVE -> checkMultiplePermission()
                            }
                        }
                    }
                }
            }
        }
    }
}

This code is not mine I just test it with my demo website.

I tried to find solution on android platform but found none.

This the android logcat

Logcat IllegalArgumentException:

024-06-03 17:28:30.668  1264-2123  AlarmManager            com.google.android.gms.persistent    W  alarm window unlikely to be respected [CONTEXT service_id=231 ] (Ask Gemini)
    java.lang.IllegalArgumentException: alarm "NetworkLocationLocator" has short window length
        at bngl.b(:com.google.android.gms@[email protected] (190800-633713831):52)
        at efni.f(:com.google.android.gms@[email protected] (190800-633713831):119)
        at efjs.r(:com.google.android.gms@[email protected] (190800-633713831):107)
        at efjs.f(:com.google.android.gms@[email protected] (190800-633713831):27)
        at efjp.apply(:com.google.android.gms@[email protected] (190800-633713831):19)
        at esdc.d(:com.google.android.gms@[email protected] (190800-633713831):3)
        at esdd.run(:com.google.android.gms@[email protected] (190800-633713831):42)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at bomx.nL(:com.google.android.gms@[email protected] (190800-633713831):1)
        at bomx.dispatchMessage(:com.google.android.gms@[email protected] (190800-633713831):138)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.os.HandlerThread.run(HandlerThread.java:67)
--------- beginning of crash
2024-06-03 17:36:43.011  1639-1639  AndroidRuntime          com.coding.meet.webviewtoapp         E  FATAL EXCEPTION: main (Ask Gemini)
    Process: com.coding.meet.webviewtoapp, PID: 1639
    java.lang.IllegalArgumentException: Can only download HTTP/HTTPS URIs: blob:https://smartappx.site/ac776f6d-9ba9-4d15-ab60-2249b485853b
        at android.app.DownloadManager$Request.<init>(DownloadManager.java:468)
        at com.coding.meet.webviewtoapp.MainActivity.download(MainActivity.kt:236)
        at com.coding.meet.webviewtoapp.MainActivity.loadWebView$lambda$2(MainActivity.kt:148)
        at com.coding.meet.webviewtoapp.MainActivity.$r8$lambda$17rajA2X3B1OvSyO9osdba9FNsQ(Unknown Source:0)
        at com.coding.meet.webviewtoapp.MainActivity$$ExternalSyntheticLambda1.onDownloadStart(Unknown Source:7)
        at g9.handleMessage(chromium-TrichromeWebViewGoogle6432.apk-stable-447211487:145)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7839)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
2024-06-03 17:39:19.433  1264-2123  AlarmManager            com.google.android.gms.persistent    W  alarm window unlikely to be respected [CONTEXT service_id=231 ] (Ask Gemini)
    java.lang.IllegalArgumentException: alarm "NetworkLocationLocator" has short window length
        at bngl.b(:com.google.android.gms@[email protected] (190800-633713831):52)
        at efni.f(:com.google.android.gms@[email protected] (190800-633713831):119)
        at efjs.r(:com.google.android.gms@[email protected] (190800-633713831):107)
        at efjs.f(:com.google.android.gms@[email protected] (190800-633713831):27)
        at efjp.apply(:com.google.android.gms@[email protected] (190800-633713831):19)
        at esdc.d(:com.google.android.gms@[email protected] (190800-633713831):3)
        at esdd.run(:com.google.android.gms@[email protected] (190800-633713831):42)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at bomx.nL(:com.google.android.gms@[email protected] (190800-633713831):1)
        at bomx.dispatchMessage(:com.google.android.gms@[email protected] (190800-633713831):138)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Solution

  • I had the similar issue the onDownloadStart param url was appended with blob. It was just in the url so I deleted it and everything works fine.

    Solution 1 :

    private fun download(
        url: String,
        userAgent: String,
        contentDisposition: String,
        mimeType: String,
        contentLength: Long
    ) {
        val folder = File(Environment.getExternalStorageDirectory().toString() + "/Download/Image")
        if (!folder.exists()) {
            folder.mkdirs()
        }
        Toast.makeText(this, "Download Started", Toast.LENGTH_SHORT).show()
    
        // Remove the 'blob:' prefix if it exists
        val cleanUrl = url.replace("blob:", "")
    
        val request = DownloadManager.Request(Uri.parse(cleanUrl))
        request.setMimeType(mimeType)
        val cookie = CookieManager.getInstance().getCookie(cleanUrl)
        request.addRequestHeader("cookie", cookie)
        request.addRequestHeader("User-Agent", userAgent)
        request.setAllowedNetworkTypes(
            DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE
        )
        val fileName = URLUtil.guessFileName(cleanUrl, contentDisposition, mimeType)
        request.setTitle(fileName)
        request.setNotificationVisibility(
            DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
        )
        request.setDestinationInExternalPublicDir(
            Environment.DIRECTORY_DOWNLOADS,
            "Image/$fileName"
        )
        val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
        downloadManager.enqueue(request)
    }
    

    Note : This is the easy way to do it


    Solution 2 :

    Another solution would be :

    1. Fetching the Blob data

    webView.setWebViewClient(new WebViewClient() {
        @Override
        public void onPageFinished(WebView view, String url) {
            view.loadUrl("javascript:(function() {" +
                "document.querySelector('a').addEventListener('click', function() {" +
                "   var xhr = new XMLHttpRequest();" +
                "   xhr.open('GET', this.href, true);" +
                "   xhr.responseType = 'blob';" +
                "   xhr.onload = function() {" +
                "       if (xhr.status === 200) {" +
                "           var reader = new FileReader();" +
                "           reader.onload = function() {" +
                "               var base64Data = reader.result.split(',')[1];" +
                "               Android.downloadBlob(base64Data, 'image/jpeg', 'myImage.jpg');" +
                "           };" +
                "           reader.readAsDataURL(xhr.response);" +
                "       }" +
                "   };" +
                "   xhr.send();" +
                "});" +
            "})();");
        }
    });
    

    2. Convert Blob to File using javascript

    webView.addJavascriptInterface(new Object() {
        @JavascriptInterface
        public void downloadBlob(String base64Data, String mimeType, String fileName) {
            byte[] data = Base64.decode(base64Data, Base64.DEFAULT);
            saveFile(data, mimeType, fileName);
        }
    }, "Android");
    

    3. Download the File

    private void saveFile(byte[] data, String mimeType, String fileName) {
        File folder = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/Image");
        if (!folder.exists()) {
            folder.mkdirs();
        }
        File file = new File(folder, fileName);
        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(data);
            Toast.makeText(this, "Download Complete", Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    
        // Notify the download manager of the new file
        DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
        downloadManager.addCompletedDownload(fileName, fileName, true, mimeType, file.getAbsolutePath(), data.length, true);
    }
    

    Use this in case my first solution doesn't work for you, since less code is always best more code is more prone to error and crashes.