简体   繁体   English

从 Android WebViewClient 中的网站下载 Blob 文件

[英]Download Blob file from Website inside Android WebViewClient

I have an HTML Web page with a button that triggers a POST request when the user clicks on.我有一个带有按钮的 HTML 网页,当用户单击时,该按钮会触发 POST 请求。 When the request is done, the following code is fired:请求完成后,将触发以下代码:

window.open(fileUrl);

Everything works great in the browser, but when implement that inside of a Webview Component, the new tab doesn't is opened.在浏览器中一切正常,但是在 Webview 组件内部实现时,新选项卡不会打开。

FYI: On my Android App, I have set the followings things:仅供参考:在我的 Android 应用程序上,我设置了以下内容:

webview.getSettings().setJavaScriptEnabled(true);
webview.getSettings().setSupportMultipleWindows(true);
webview.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);

On the AndroidManifest.xml I have the following permissions:AndroidManifest.xml我有以下权限:

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

I try too with a setDownloadListener to catch the download.我也尝试使用setDownloadListener来捕获下载。 Another approach was replaced the WebViewClient() for WebChromeClient() but the behavior was the same.另一种方法被替换为WebChromeClient()WebViewClient() ) 但行为是相同的。

Ok I had the same problem working with webviews, I realized that WebViewClient can't load "blob URLs" as Chrome Desktop client does.好的,我在使用 webviews 时遇到了同样的问题,我意识到 WebViewClient 无法像 Chrome 桌面客户端那样加载“blob URL”。 I solved it using Javascript Interfaces.我使用Javascript接口解决了它。 You can do this by following the steps below and it works fine with minSdkVersion: 17. First, transform the Blob URL data in Base64 string using JS.您可以按照以下步骤执行此操作,它与 minSdkVersion 一起工作正常: 17. 首先,使用 JS 将 Blob URL 数据转换为 Base64 字符串。 Second, send this string to a Java Class and finally convert it in an available format, in this case I converted it in a ".pdf" file.其次,将此字符串发送到 Java 类,最后将其转换为可用格式,在这种情况下,我将其转换为“.pdf”文件。

Before continue you can download the source code here :).在继续之前,您可以在此处下载源代码 :)。 The app is developed in Kotlin and Java.该应用程序是用 Kotlin 和 Java 开发的。 If you find any error, please let me know and I will fix it:如果您发现任何错误,请告诉我,我会修复它:

https://github.com/JaegerCodes/AmazingAndroidWebview https://github.com/JaegerCodes/AmazingAndroidWebview

First things first.第一件事。 You have to setup your webview.您必须设置您的网络视图。 In my case I'm loading the webpages in a fragment:就我而言,我正在将网页加载到片段中:

public class WebviewFragment extends Fragment {
    WebView browser;
    ...
 
    // invoke this method after set your WebViewClient and ChromeClient
    private void browserSettings() {
        browser.getSettings().setJavaScriptEnabled(true);
        browser.setDownloadListener(new DownloadListener() {
            @Override
            public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) {
                browser.loadUrl(JavaScriptInterface.getBase64StringFromBlobUrl(url));
            }
        });
        browser.getSettings().setAppCachePath(getActivity().getApplicationContext().getCacheDir().getAbsolutePath());
        browser.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
        browser.getSettings().setDatabaseEnabled(true);
        browser.getSettings().setDomStorageEnabled(true);
        browser.getSettings().setUseWideViewPort(true);
        browser.getSettings().setLoadWithOverviewMode(true);
        browser.addJavascriptInterface(new JavaScriptInterface(getContext()), "Android");
        browser.getSettings().setPluginState(PluginState.ON);
    }
}

Finally, create a JavaScriptInterface class.最后,创建一个 JavaScriptInterface 类。 This class contains the script that is going to be executed in our webpage.此类包含将在我们的网页中执行的脚本。

public class JavaScriptInterface {
    private Context context;
    public JavaScriptInterface(Context context) {
        this.context = context;
    }

    @JavascriptInterface
    public void getBase64FromBlobData(String base64Data) throws IOException {
        convertBase64StringToPdfAndStoreIt(base64Data);
    }
    public static String getBase64StringFromBlobUrl(String blobUrl) {
        if(blobUrl.startsWith("blob")){
            return "javascript: var xhr = new XMLHttpRequest();" +
                    "xhr.open('GET', '"+ blobUrl +"', true);" +
                    "xhr.setRequestHeader('Content-type','application/pdf');" +
                    "xhr.responseType = 'blob';" +
                    "xhr.onload = function(e) {" +
                    "    if (this.status == 200) {" +
                    "        var blobPdf = this.response;" +
                    "        var reader = new FileReader();" +
                    "        reader.readAsDataURL(blobPdf);" +
                    "        reader.onloadend = function() {" +
                    "            base64data = reader.result;" +
                    "            Android.getBase64FromBlobData(base64data);" +
                    "        }" +
                    "    }" +
                    "};" +
                    "xhr.send();";
        }
        return "javascript: console.log('It is not a Blob URL');";
    }
    private void convertBase64StringToPdfAndStoreIt(String base64PDf) throws IOException {
        final int notificationId = 1;
        String currentDateTime = DateFormat.getDateTimeInstance().format(new Date());
        final File dwldsPath = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DOWNLOADS) + "/YourFileName_" + currentDateTime + "_.pdf");
        byte[] pdfAsBytes = Base64.decode(base64PDf.replaceFirst("^data:application/pdf;base64,", ""), 0);
        FileOutputStream os;
        os = new FileOutputStream(dwldsPath, false);
        os.write(pdfAsBytes);
        os.flush();

        if (dwldsPath.exists()) {
            Intent intent = new Intent();
            intent.setAction(android.content.Intent.ACTION_VIEW);
            Uri apkURI = FileProvider.getUriForFile(context,context.getApplicationContext().getPackageName() + ".provider", dwldsPath);
            intent.setDataAndType(apkURI, MimeTypeMap.getSingleton().getMimeTypeFromExtension("pdf"));
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            PendingIntent pendingIntent = PendingIntent.getActivity(context,1, intent, PendingIntent.FLAG_CANCEL_CURRENT);
            String CHANNEL_ID = "MYCHANNEL";
            final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                NotificationChannel notificationChannel= new NotificationChannel(CHANNEL_ID,"name", NotificationManager.IMPORTANCE_LOW);
                Notification notification = new Notification.Builder(context,CHANNEL_ID)
                        .setContentText("You have got something new!")
                        .setContentTitle("File downloaded")
                        .setContentIntent(pendingIntent)
                        .setChannelId(CHANNEL_ID)
                        .setSmallIcon(android.R.drawable.sym_action_chat)
                        .build();
                if (notificationManager != null) {
                    notificationManager.createNotificationChannel(notificationChannel);
                    notificationManager.notify(notificationId, notification);
                }

            } else {
                NotificationCompat.Builder b = new NotificationCompat.Builder(context, CHANNEL_ID)
                        .setDefaults(NotificationCompat.DEFAULT_ALL)
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(android.R.drawable.sym_action_chat)
                        //.setContentIntent(pendingIntent)
                        .setContentTitle("MY TITLE")
                        .setContentText("MY TEXT CONTENT");

                if (notificationManager != null) {
                    notificationManager.notify(notificationId, b.build());
                    Handler h = new Handler();
                    long delayInMilliseconds = 1000;
                    h.postDelayed(new Runnable() {
                        public void run() {
                            notificationManager.cancel(notificationId);
                        }
                    }, delayInMilliseconds);
                }
            }
        }
        Toast.makeText(context, "PDF FILE DOWNLOADED!", Toast.LENGTH_SHORT).show();
    }
}
 

EXTRA: If you want to share these downloaded files with other Apps create an xml file in: ..\res\xml\provider_paths.xml额外:如果您想与其他应用程序共享这些下载的文件,请在以下位置创建一个 xml 文件:..\res\xml\provider_paths.xml

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

Finally add this provider to your AndroidManifest.xml file最后将此提供程序添加到您的 AndroidManifest.xml 文件中

<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>
        <!-- some code below ->

Another approach is by using "Chrome Custom Tabs"另一种方法是使用“Chrome 自定义标签”

Java:爪哇:

CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
    CustomTabsIntent customTabsIntent = builder.build();
    customTabsIntent.launchUrl(context, Uri.parse("https://stackoverflow.com"));

Kotlin:科特林:

val url = "https://stackoverflow.com/"
            val builder = CustomTabsIntent.Builder()
            val customTabsIntent = builder.build()
            customTabsIntent.launchUrl(this, Uri.parse(url))

Sources:资料来源:

https://stackoverflow.com/a/41339946/4001198 https://stackoverflow.com/a/41339946/4001198

https://stackoverflow.com/a/11901662/4001198 https://stackoverflow.com/a/11901662/4001198

https://stackoverflow.com/a/19959041/4001198 https://stackoverflow.com/a/19959041/4001198

https://developer.android.com/training/secure-file-sharing/setup-sharing https://developer.android.com/training/secure-file-sharing/setup-sharing

I recently faced similar issues on Android.我最近在 Android 上遇到了类似的问题。 I was able to find a work around thanks to this thread!由于这个线程,我能够找到解决方法!

I reused & refactored the code snippet shared above in Kotlin我重用并重构了上面在 Kotlin 中共享的代码片段

Explanation : WebViewClient can't load Blob URL.解释:WebViewClient 无法加载 Blob URL。 A work around would be to convert Blob URL to a Blob Object, then to a Base64 data on the web side.一种解决方法是将 Blob URL 转换为 Blob 对象,然后转换为 Web 端的 Base64 数据。 The native side will download the attachment in Base64 data according to the mime type specified in the prefix of the Base64 data. Native端会根据Base64数据前缀中指定的mime类型下载Base64数据中的附件。

JavascriptInterface.kt JavascriptInterface.kt

import android.content.Context
import android.os.Environment
import android.util.Base64
import android.util.Log
import android.webkit.JavascriptInterface
import android.widget.Toast
import java.io.File
import java.io.FileOutputStream

class JavascriptInterface {
    var context: Context;

    constructor(context: Context) {
        this.context = context;
    }

    /**
     * Method to process Base64 data then save it locally.
     *
     * 1. Strip Base64 prefix from Base64 data
     * 2. Decode Base64 data
     * 3. Write Base64 data to file based on mime type located in prefix
     * 4. Save file locally
     */
    @JavascriptInterface
    fun processBase64Data(base64Data: String) {
        Log.i("JavascriptInterface/processBase64Data", "Processing base64Data ...")

        var fileName = "";
        var bytes = "";

        if (base64Data.startsWith("data:image/png;base64,")) {
            fileName = "foo.png"
            bytes = base64Data.replaceFirst("data:image/png;base64,","")
        }

        if (fileName.isNotEmpty() && bytes.isNotEmpty()) {
            val downloadPath = File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
                fileName
            )

            Log.i("JavascriptInterface/processBase64Data", "Download Path: ${downloadPath.absolutePath}")

            val decodedString = Base64.decode(bytes, Base64.DEFAULT)
            val os = FileOutputStream(downloadPath, false)
            os.write(decodedString)
            os.flush()
        }
    }

    /**
     * Method to convert blobUrl to Blob, then process Base64 data on native side
     *
     * 1. Download Blob URL as Blob object
     * 2. Convert Blob object to Base64 data
     * 3. Pass Base64 data to Android layer for processing
     */
    fun getBase64StringFromBlobUrl(blobUrl: String): String {
        Log.i("JavascriptInterface/getBase64StringFromBlobUrl", "Downloading $blobUrl ...")

        // Script to convert blob URL to Base64 data in Web layer, then process it in Android layer
        val script = "javascript: (() => {" +
            "async function getBase64StringFromBlobUrl() {" +
            "const xhr = new XMLHttpRequest();" +
            "xhr.open('GET', '${blobUrl}', true);" +
            "xhr.setRequestHeader('Content-type', 'image/png');" +
            "xhr.responseType = 'blob';" +
            "xhr.onload = () => {" +
            "if (xhr.status === 200) {" +
            "const blobResponse = xhr.response;" +
            "const fileReaderInstance = new FileReader();" +
            "fileReaderInstance.readAsDataURL(blobResponse);" +
            "fileReaderInstance.onloadend = () => {" +
            "console.log('Downloaded' + ' ' + '${blobUrl}' + ' ' + 'successfully!');" +
            "const base64data = fileReaderInstance.result;" +
            "Android.processBase64Data(base64data);" +
            "}" + // file reader on load end
            "}" + // if
            "};" + // xhr on load
            "xhr.send();" +
            "}" + // async function
            "getBase64StringFromBlobUrl();" +
            "}) ()"

        return script
    }
}

MainActivity.kt MainActivity.kt

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v7.app.AppCompatActivity
import android.webkit.DownloadListener
import android.webkit.WebView
import java.net.URL

class MainActivity : AppCompatActivity() {
    var debug = true

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

        // Request permissions
        ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), PackageManager.PERMISSION_GRANTED);

        val wv = findViewById<WebView>(R.id.web_view)
        wv.settings.javaScriptEnabled = true
        wv.settings.domStorageEnabled = true

        // Load local .html with baseUrl set to production domain since attachment downloads does not work cross-origin
        val queryParams = "foo=bar"
        var url = URL(OmnichannelConfig.config["src"])
        val baseUrl = "${url.protocol}://${url.host}?${queryParams}"
        val data = application.assets.open("index.html").bufferedReader().use {
            it.readText()
        };

        wv.loadDataWithBaseURL(baseUrl, data, "text/html", null, baseUrl)

        // Expose Android methods to Javascript layer
        val javascriptInterface = JavascriptInterface(applicationContext)
        wv.addJavascriptInterface(javascriptInterface, "Android")

        // Subscribe to notification when a file from Web content needs to be downloaded in Android layer
        wv.setDownloadListener(DownloadListener { url, _, _, _, _ ->
            if (url.startsWith("blob:")) {
                wv.evaluateJavascript(javascriptInterface.getBase64StringFromBlobUrl(url), null)
            }
        })
    }
}

AndroidManifest.xml AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.demo">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Note : Inside JavascriptInterface.kt 's processBase64Data method, only data:image/png;base64, is being handled.注意:在JavascriptInterface.kt的 processBase64Data 方法中,只有data:image/png;base64,正在处理。 Additional implementation is required to handle data with different mime types ( data:application/pdf;base64, , data:image/gif;base64, , data:image/png;base64, , etc)需要额外的实现来处理具有不同 mime 类型的数据( data:application/pdf;base64, , data:image/gif;base64, , data:image/png;base64, , 等)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM