• 手机端 Android WebView 获取 blob 链接文件名并下载网页动态生成的 pdf 文件且调用外部程序打开


    目录

    1、已有可实用的动态 pdf 生成方案,

    2、安卓编辑器中建立 Layout ,添加 WebView :

    3、extends Activity 将上面的 WebView 赋值给变量(实例化?),并设置其大堆属性,以扩展其功能:

    4、引入参考资料 1 的方法,定义下载完成监听器

    5、获取 Blob 链接文件名及类型

    6、重写 WebView 的下载监听器 DownloadListener 

    7、其中的几个变量及其他说明


    参考资料:

    1.Android WebView支持下载blob协议文件_Misdirection_XG的博客-CSDN博客_android blob

    2. Android Webview实现文件下载功能 - huidaoli - 博客园

    3.base64和Blob互相转换_weixin_30776863的博客-CSDN博客

    4.Android:你要的WebView与JS交互方式都在这里了 - 百度文库

    应用于:html转pdf文件下载之最合理的方法支持中文_jessezappy的博客-CSDN博客

    项目需求:

    1.网页动态生成 pdf 文件。

    2.手机打开以上网页下载 pdf 文件,并用外部程序打开 pdf 文件。

    之前的文章:html转pdf文件下载之最合理的方法支持中文_jessezappy的博客-CSDN博客 ,中已经实现了网页动态生成 pdf 文件,通过好多种方式都可在 PC 端实现自动下载动态生成的 pdf 文件,如:

    pdf.save("A4.pdf") ;

    但我项目原计划是用手机打开这个网页下载 pdf 的,那些方法到了手机端,用 WebView 打开网页后,均无法下载由网页 js 生成的 pdf 文档。

    深度研究 jspdf.umd.js 后,发现,其输出 pdf 数据有好多种参数:

    1. /**
    2. * Generates the PDF document.
    3. *
    4. * If `type` argument is undefined, output is raw body of resulting PDF returned as a string.
    5. *
    6. * @param {string} type A string identifying one of the possible output types.
    7. * Possible values are:
    8. * 'arraybuffer' -> (ArrayBuffer)
    9. * 'blob' -> (Blob)
    10. * 'bloburi'/'bloburl' -> (string)
    11. * 'datauristring'/'dataurlstring' -> (string)
    12. * 'datauri'/'dataurl' -> (undefined) -> change location to generated datauristring/dataurlstring
    13. * 'dataurlnewwindow' -> (window | null | undefined) throws error if global isn't a window object(node)
    14. * 'pdfobjectnewwindow' -> (window | null) throws error if global isn't a window object(node)
    15. * 'pdfjsnewwindow' -> (wind | null)
    16. * @param {Object|string} options An object providing some additional signalling to PDF generator.
    17. * Possible options are 'filename'.
    18. * A string can be passed instead of {filename:string} and defaults to 'generated.pdf'
    19. * @function
    20. * @instance
    21. * @returns {string|window|ArrayBuffer|Blob|jsPDF|null|undefined}
    22. * @memberof jsPDF#
    23. * @name output
    24. */
    25. var output = API.output = API.__private__.output = SAFE(function output(type, options) {
    26. options = options || {};
    27. if (typeof options === "string") {
    28. options = {
    29. filename: options
    30. };
    31. } else {
    32. options.filename = options.filename || "generated.pdf";
    33. }
    34. switch (type) {
    35. case undefined:
    36. return buildDocument();
    37. case "save":
    38. API.save(options.filename);
    39. break;
    40. case "arraybuffer":
    41. return getArrayBuffer(buildDocument());
    42. case "blob":
    43. return getBlob(buildDocument());
    44. case "bloburi":
    45. case "bloburl":
    46. // Developer is responsible of calling revokeObjectURL
    47. if (typeof globalObject.URL !== "undefined" && typeof globalObject.URL.createObjectURL === "function") {
    48. return globalObject.URL && globalObject.URL.createObjectURL(getBlob(buildDocument())) || void 0;
    49. } else {
    50. console.warn("bloburl is not supported by your system, because URL.createObjectURL is not supported by your browser.");
    51. }
    52. break;
    53. case "datauristring":
    54. case "dataurlstring":
    55. var dataURI = "";
    56. var pdfDocument = buildDocument();
    57. try {
    58. dataURI = btoa(pdfDocument);
    59. } catch (e) {
    60. dataURI = btoa(unescape(encodeURIComponent(pdfDocument)));
    61. }
    62. return "data:application/pdf;filename=" + options.filename + ";base64," + dataURI;
    63. case "pdfobjectnewwindow":
    64. if (Object.prototype.toString.call(globalObject) === "[object Window]") {
    65. var pdfObjectUrl = "https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js";
    66. var integrity = ' integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==" crossorigin="anonymous"';
    67. if (options.pdfObjectUrl) {
    68. pdfObjectUrl = options.pdfObjectUrl;
    69. integrity = "";
    70. }
    71. var htmlForNewWindow = "" + '";
    72. var nW = globalObject.open();
    73. if (nW !== null) {
    74. nW.document.write(htmlForNewWindow);
    75. }
    76. return nW;
    77. } else {
    78. throw new Error("The option pdfobjectnewwindow just works in a browser-environment.");
    79. }
    80. case "pdfjsnewwindow":
    81. if (Object.prototype.toString.call(globalObject) === "[object Window]") {
    82. var pdfJsUrl = options.pdfJsUrl || "./examples/PDF.js/web/viewer.html";
    83. var htmlForPDFjsNewWindow = "" + "" + '' + "";
    84. var dataURLNewWindow = globalObject.open();
    85. if (dataURLNewWindow !== null) {
    86. dataURLNewWindow.document.write(htmlForDataURLNewWindow);
    87. dataURLNewWindow.document.title = options.filename;
    88. }
    89. if (dataURLNewWindow || typeof safari === "undefined") return dataURLNewWindow;
    90. } else {
    91. throw new Error("The option dataurlnewwindow just works in a browser-environment.");
    92. }
    93. break;
    94. case "datauri":
    95. case "dataurl":
    96. return globalObject.document.location.href = this.output("datauristring", options);
    97. default:
    98. return null;
    99. }
    100. });

    以上这些参数,在 CallBack 中调用 pdf.Output 时使用:

    1. var link = document.getElementById('linklink');
    2. link.target = '_blank';
    3. //link.href = window.URL.createObjectURL(convertBase64UrlToBlob(pdf.output('datauristring',{filename: 'A4.pdf'})));//140ms Base64 数据转 Blob
    4. //link.href = window.URL.createObjectURL(pdf.output('blob',{filename: 'A4.pdf'}));//77ms Base64 数据
    5. link.href = pdf.output('bloburi');//77ms 直接输出Blob 链接
    6. //link.href =
    7. //pdf.output('pdfobjectnewwindow',{filename: 'A41.pdf'});//弹出对象窗口,无用
    8. //link.href = pdf.output('dataurl',{filename: 'A4.pdf'});//数据链接无用
    9. //pdf.output('pdfjsnewwindow',{filename: 'A42.pdf'});//弹出窗口
    10. //pdf.output('dataurlnewwindow',{filename: 'A43.pdf'});//弹出窗口,无用
    11. link.download ="A41.pdf";
    12. link.text='点击这里下载';

    经过对比,发现在不考虑后台生成 pdf 文件的情况下,只有生成 Blob 链接适合用于手机前台下载,但只有华为自带浏览器支持下载 blob 链接而且还必须用华为浏览器打开那个网页才行,QQ、百度等均不支持下载 blob 链接。

    jspdf 中原例子里面使用的是 iframe.src = pdf.output('datauristring'); 方式,返回的是 Base64 编码的 pdf 数据,可以使用参考资料 3 中的 convertBase64UrlToBlob 转换为 Blob 链接,分析 jspdf 的 Out 方法后发现其可直接输出 Blob 链接,那么就暂时用不到参考资料 3 中的方法了。

    最终决定,由网页 JS 生成 Blob 链接给 A 标签,在手机端点击这个 A 标签下载为 pdf 文档,并打开。

    (刚刚写了一大段,按了下 Ctrl+z 就全没了,草稿也没了,是要我重新梳理下吗???)

    那么,就重新梳理,整理一下解决方法好了,零碎的分析就不写了。

    那么,项目解决方案步骤开始:

    1、已有可实用的动态 pdf 生成方案,详见:

    html转pdf文件下载之最合理的方法支持中文_jessezappy的博客-CSDN博客

    2、安卓编辑器中建立 Layout ,添加 WebView :

    1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    2. android:id="@+id/webfrm"
    3. android:layout_width="fill_parent"
    4. android:layout_height="fill_parent" >
    5. <WebView
    6. android:id="@+id/webshow"
    7. android:layout_width="match_parent"
    8. android:layout_height="fill_parent"
    9. android:layout_alignParentLeft="true" />
    10. RelativeLayout>

    3、extends Activity 将上面的 WebView 赋值给变量(实例化?),并设置其大堆属性,以扩展其功能:

    WebView mWebView;

    onCreate 中赋值 mWebView:

    1. mWebView=(WebView)findViewById(R.id.webshow);
    2. setWebStyle();

    设置 mWebView 属性及重写其部分动作,扩展功能:

    1. @SuppressLint("SetJavaScriptEnabled")
    2. private void setWebStyle() {
    3. WebSettings webseting = mWebView.getSettings();
    4. webseting.setAppCachePath(getApplicationContext().getCacheDir().getAbsolutePath());
    5. webseting.setUseWideViewPort(true);
    6. webseting.setLoadWithOverviewMode(true);
    7. //webseting.setPluginState(WebSettings.PluginState.ON);
    8. webseting.setDomStorageEnabled(true);//最重要的方法,一定要设置,这就是出不来的主要原因 //webseting.setDomStorageEnabled(true);
    9. webseting.setSupportZoom(true);
    10. webseting.setDefaultTextEncodingName("utf-8");
    11. /* 下载blob准备 */
    12. webseting.setJavaScriptEnabled(true);
    13. webseting.setJavaScriptCanOpenWindowsAutomatically(true);
    14. /***********************/
    15. mWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);// 去掉底部和右边的滚动条
    16. mWebView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); // 去掉底部和右边的滚动条
    17. mWebView.requestFocus();
    18. webseting.setCacheMode(WebSettings.LOAD_DEFAULT); // 默认使用缓存
    19. // webseting.setCacheMode(WebSettings.LOAD_NO_CACHE); //默认不使用缓存!
    20. webseting.setAppCacheMaxSize(1024*1024*20);//设置缓冲大小,便于第一次缓存字体,否则每次下载字体需时太长。
    21. String appCacheDir=this.getApplicationContext().getDir("cache",Context.MODE_PRIVATE).getPath();
    22. webseting.setAppCachePath(appCacheDir);
    23. webseting.setAllowFileAccess(true);
    24. webseting.setAppCacheEnabled(true);
    25. webseting.setCacheMode(WebSettings.LOAD_DEFAULT|WebSettings.LOAD_CACHE_ELSE_NETWORK);
    26. //webview 动作重写
    27. mWebView.setWebViewClient(new WebViewClient(){
    28. @Override
    29. public boolean shouldOverrideUrlLoading(WebView view, String url) {
    30. view.loadUrl(url);
    31. return false;// false 显示frameset, true 不显示Frameset ,内嵌页面
    32. //return true;
    33. }
    34. @Override
    35. public void onPageStarted(WebView view, String url, Bitmap favicon) {
    36. //有页面跳转时被回调
    37. //dialog = ProgressDialog.show(webxj.this,null,"数据加载中,请稍侯...");
    38. super.onPageStarted(view, url,favicon);
    39. }
    40. @Override
    41. public void onPageFinished(WebView view, String url) {
    42. //页面跳转结束后被回调
    43. //dialog.dismiss();
    44. super.onPageFinished(view, url);
    45. }
    46. @Override
    47. public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
    48. super.onReceivedError(view, errorCode, description, failingUrl);//错误处理+ description
    49. Toast.makeText(webxj.this, "错误:请检查网络链接! " , Toast.LENGTH_SHORT).show();
    50. dialog.dismiss();
    51. new Handler().postDelayed(new Runnable() {
    52. public void run() {
    53. mWebView.loadUrl(urlw); //显示等待画面
    54. }
    55. }, 500);
    56. }
    57. });
    58. //禁用webview右键功能:长按
    59. mWebView.setOnLongClickListener(new OnLongClickListener(){
    60. public boolean onLongClick(View v) {
    61. // TODO Auto-generated method stub
    62. return true;
    63. }
    64. });
    65. //blob
    66. //下载支持,A 标签内需 download
    67. mWebView.setDownloadListener(new MyWebViewDownLoadListener()); // 设置 WebView 下载监听器
    68. mWebView.addJavascriptInterface(mDownloadBlobFileJSInterface, "Android"); // Blob 下载 js 定义
    69. mDownloadBlobFileJSInterface.setDownloadGifSuccessListener(new openDownloadfile()); // Blob 下载完成后监听器
    70. }

    4、引入参考资料 1 的方法,定义下载完成监听器

    注:上面步骤 3 中已正确设置下载完成监听器:

    mDownloadBlobFileJSInterface.setDownloadGifSuccessListener(new openDownloadfile()); //下载完成后监听器

    参考资料 1 中的:

    mDownloadBlobFileJSInterface.setDownloadGifSuccessListener(absolutePath -> Toast.makeText(MainActivity.this,"下载成功,在Download目录下",Toast.LENGTH_SHORT).show());

    这句有问题,absolutePath 未定义,怀疑其是抄来的代码,没抄全,分析发现其为定义下载完成监听器,那么,就按照重写 DownloadListener 的定义,自己定义一个监听器,用于下载完成后的处理,如调用外部程序打开:

    1. /* 定义下载完成事件监听器 */
    2. private class openDownloadfile implements DownloadGifSuccessListener {
    3. public void ondownloadGifSuccess(String gifFile){
    4. String fileName=gifFile.substring(gifFile.lastIndexOf("/")+1);
    5. String directory=gifFile.substring(0,gifFile.lastIndexOf("/")+1);
    6. // System.out.println("11.下载成功,fileName:"+fileName);
    7. // System.out.println("12.下载成功,directory:"+directory);
    8. File File = new File(directory,fileName);
    9. Intent intent = getFileIntent(File);
    10. startActivity(intent); //调用万部程序打开
    11. // System.out.println("13.下载成功,在Download目录下:"+gifFile);
    12. }
    13. }
    14. public Intent getFileIntent(File file){
    15. // Uri uri = Uri.parse("http://m.ql18.com.cn/hpf10/1.pdf");
    16. Uri uri = Uri.fromFile(file);
    17. String type = getMIMEType(file);
    18. // Log.e("tag", "type="+type);
    19. Intent intent = new Intent("android.intent.action.VIEW");
    20. intent.addCategory("android.intent.category.DEFAULT");
    21. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    22. intent.setDataAndType(uri, type);
    23. return intent;
    24. }
    25. private String getMIMEType(File f){
    26. String type="";
    27. String fName=f.getName();
    28. // * 取得扩展名 * /
    29. String end=fName.substring(fName.lastIndexOf(".")+1,fName.length());//.toLowerCase();
    30. Locale loc = Locale.getDefault();//可以去掉这步
    31. end=end.toLowerCase(loc);
    32. // * 依扩展名的类型决定MimeType * /
    33. if(end.equals("pdf")){
    34. type = "application/pdf";//
    35. }
    36. else if(end.equals("m4a")||end.equals("mp3")||end.equals("mid")||
    37. end.equals("xmf")||end.equals("ogg")||end.equals("wav")){
    38. type = "audio/*";
    39. }
    40. else if(end.equals("3gp")||end.equals("mp4")){
    41. type = "video/*";
    42. }
    43. else if(end.equals("jpg")||end.equals("gif")||end.equals("png")||
    44. end.equals("jpeg")||end.equals("bmp")){
    45. type = "image/*";
    46. }
    47. else if(end.equals("apk")){
    48. // * android.permission.INSTALL_PACKAGES * /
    49. type = "application/vnd.android.package-archive";
    50. }
    51. // else if(end.equals("pptx")||end.equals("ppt")){
    52. // type = "application/vnd.ms-powerpoint";
    53. // }else if(end.equals("docx")||end.equals("doc")){
    54. // type = "application/vnd.ms-word";
    55. // }else if(end.equals("xlsx")||end.equals("xls")){
    56. // type = "application/vnd.ms-excel";
    57. // }
    58. else{
    59. // // * 如果无法直接打开,就跳出软件列表给用户选择 * /
    60. type="*/*"; //因注释多加了一个空格
    61. }
    62. return type;
    63. }

    其中 getFileIntent 和 getMIMEType 来自参考资料 2 。

    参考资料 1 中的 DownloadBlobFileJSInterface 可直接使用,等下 获取 Blob 文件名时才需修改其转换 Base64 及保存过程。

    5、获取 Blob 链接文件名及类型

    参考资料 1 中,是无法取得 Blob 文件名及类型的,那么就要用个变通的方法:调用网页预设的 JS 中的 getDname 函数,获取文件名,文件名已事先由 .html 方法的回调函数中预设。

    6、重写 WebView 的下载监听器 DownloadListener 

    注:下载监听器已在 setWebStyle(); 中设置:

    mWebView.setDownloadListener(new MyWebViewDownLoadListener());

    在下载监听器中判断是否为 Blob 链接,不是则启用参考资料 2 中的直接下载进程。

    是的话,则启用参考资料 1 中的 Blob 下载进程,并在下载前调用网页中的 getDname js 函数获取预设的 Blob 文件名:

    1. // 改装blob下载
    2. private class MyWebViewDownLoadListener implements DownloadListener {
    3. public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype,long contentLength){
    4. if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
    5. Toast.makeText(webxj.this, "需要SD卡。", Toast.LENGTH_SHORT).show();
    6. return;
    7. }
    8. //Toast.makeText(webxj.this,"Url:"+url.lastIndexOf("data:app"), Toast.LENGTH_SHORT).show();
    9. // System.out.println("Url:"+url);
    10. // Log.e("tag", "Url="+url);
    11. if(url.indexOf("blob:http")==0){
    12. urlP=url;
    13. // System.out.println("打开:"+urlP);
    14. //mWebView.loadUrl("javascript:calljs();");
    15. mWebView.evaluateJavascript("javascript:getDname('"+urlP+"')", new ValueCallback(){
    16. public void onReceiveValue(String value){
    17. // System.out.println("JS返回::"+value);
    18. pdffn=value.replace("\"","");
    19. // System.out.println("pdffn:"+pdffn);
    20. File directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
    21. File file=new File(directory,pdffn);
    22. if(file.exists()){
    23. // Log.e("tag", "The file has already exists."); //文件已存在
    24. System.out.println("文件已存在:"+directory+pdffn);
    25. Toast.makeText(webxj.this, "文件已保存:"+directory+"/"+pdffn,Toast.LENGTH_LONG).show();
    26. File File = new File(directory,pdffn);
    27. Intent intent = getFileIntent(File);
    28. startActivity(intent);
    29. }else{
    30. mWebView.loadUrl(DownloadBlobFileJSInterface.getBase64StringFromBlobUrl(urlP));
    31. }
    32. }
    33. });
    34. //mWebView.loadUrl(DownloadBlobFileJSInterface.getBase64StringFromBlobUrl(url));
    35. }else{
    36. DownloaderTask task=new DownloaderTask();
    37. task.execute(url);
    38. }
    39. }
    40. }

    注意:网页 JS 返回的字符串前后有双引号 " 包裹,需要将其去除。

    其中, DownloaderTask 为参考资料 2 的直接下载方法,注意,A 标签中必须加上 download ,否则报错:

    <a href="./upload/dll.rar" download>dll.rara>

     然后,在 onCreate 打开动态生成 pdf 文件的页面即可:

    1. double rndX=Math.random();
    2. urlx="?v="+String.valueOf(rndX);
    3. urlm=getString(R.string.mainUrl);//
    4. mWebView=(WebView)findViewById(R.id.webshow);
    5. setWebStyle();
    6. mWebView.loadUrl(urlm + urlx);

    7、其中的几个变量及其他说明

    1. // 定义于 extends Activity 与 onCreate 之间
    2. WebView mWebView; // WebView 实例变量
    3. private String urlx=new String(""); //存放调用首页的参数等,如 double rndX=Math.random(); urlx="?v="+String.valueOf(rndX);
    4. private String urlw=new String("file:///android_asset/index.html"); //存放等待页面url
    5. private String urlm;//存放首页菜单的页面
    6. DownloadBlobFileJSInterface mDownloadBlobFileJSInterface = new DownloadBlobFileJSInterface(this); // Blob 下载 js 接口定义
    7. private String urlP=new String(""); // 下载链接全局变量
    8. private static String pdffn=new String(""); // Blob 文件名全局变量

    其中 pdffn 用于存放 Blob 链接预设文件名,因为我懒得其动 DownloadBlobFileJSInterface 的输入变量,就用这个传递给其中的 convertToGifAndProcess 方法,用做保存文件名:

    1. /**
    2. * 转换成file
    3. * @param base64
    4. */
    5. private void convertToGifAndProcess(String base64) {
    6. String fileName =pdffn;// UUID.randomUUID().toString() + ".pdf";
    7. // System.out.println("3.convertToGifAndProcess:fileName:" + fileName);
    8. File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
    9. File gifFile = new File(directory, fileName);
    10. // System.out.println("4.convertToGifAndProcess:gifFile:" + gifFile);
    11. // Log.e("tag", "type=convertToGifAndProcess" + gifFile);
    12. saveFileToPath(base64, gifFile);
    13. // System.out.println("7.convertToGifAndProcess:mDownloadGifSuccessListener:" + mDownloadGifSuccessListener);
    14. // System.out.println("8.convertToGifAndProcess gifFile:" + gifFile);
    15. if (mDownloadGifSuccessListener != null) {
    16. // System.out.println("10.mDownloadGifSuccessListener 不为空到这里:" + mDownloadGifSuccessListener);
    17. mDownloadGifSuccessListener.ondownloadGifSuccess(gifFile.getAbsolutePath());
    18. }
    19. }
    20. /**
    21. * 保存文件
    22. * @param base64
    23. * @param gifFilePath
    24. */
    25. private void saveFileToPath(String base64, File gifFilePath) {
    26. // System.out.println("5.saveFileToPath gifFilePath.getAbsolutePath():" + gifFilePath.getAbsolutePath());
    27. try {
    28. byte[] fileBytes = Base64.decode(base64.replaceFirst(
    29. "data:application/pdf;base64,", ""), 0);
    30. FileOutputStream os = new FileOutputStream(gifFilePath, false);
    31. os.write(fileBytes);
    32. os.flush();
    33. os.close();
    34. Toast.makeText(mContext, "文件已保存:"+gifFilePath,Toast.LENGTH_LONG).show();
    35. // Log.e("tag", "type=saveFileToPath" + gifFilePath);
    36. // System.out.println("6.saveFileToPath FileOutputStream gifFilePath:" + gifFilePath);
    37. } catch (Exception e) {
    38. e.printStackTrace();
    39. }
    40. }

    至此,完整的流程及关键代码完成,下面是运行后的样例:

     

     

    ’-------------------------------

    此记!

    接下来还要解决下手机端显示宽度问题

  • 相关阅读:
    代码随想录第56天
    Adobe Acrobat 编辑器软件下载安装,Acrobat 轻松编辑和管理各种PDF文件
    在Web服务器(IIS)上安装SSL证书并绑定网站
    Hardhat开发智能合约和DApp
    打造全身视角的医院可视化能源监测管理平台,实现医院能源可视化管理
    MySQL面试题——MySQL常见查询
    DTDX991A 61430001-UW 自由IOT引入人工智能功能
    KeyDB源码解析四——其他特性
    设计模式-单例模式
    threejs开发太阳系案例
  • 原文地址:https://blog.csdn.net/jessezappy/article/details/126264165