手机端 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 数据有好多种参数:

    /**
     * Generates the PDF document.
     *
     * If `type` argument is undefined, output is raw body of resulting PDF returned as a string.
     *
     * @param {string} type A string identifying one of the possible output types.
* Possible values are:
* 'arraybuffer' -> (ArrayBuffer)
* 'blob' -> (Blob)
* 'bloburi'/'bloburl' -> (string)
* 'datauristring'/'dataurlstring' -> (string)
* 'datauri'/'dataurl' -> (undefined) -> change location to generated datauristring/dataurlstring
* 'dataurlnewwindow' -> (window | null | undefined) throws error if global isn't a window object(node)
* 'pdfobjectnewwindow' -> (window | null) throws error if global isn't a window object(node)
* 'pdfjsnewwindow' -> (wind | null) * @param {Object|string} options An object providing some additional signalling to PDF generator.
* Possible options are 'filename'.
* A string can be passed instead of {filename:string} and defaults to 'generated.pdf' * @function * @instance * @returns {string|window|ArrayBuffer|Blob|jsPDF|null|undefined} * @memberof jsPDF# * @name output */ var output = API.output = API.__private__.output = SAFE(function output(type, options) { options = options || {}; if (typeof options === "string") { options = { filename: options }; } else { options.filename = options.filename || "generated.pdf"; } switch (type) { case undefined: return buildDocument(); case "save": API.save(options.filename); break; case "arraybuffer": return getArrayBuffer(buildDocument()); case "blob": return getBlob(buildDocument()); case "bloburi": case "bloburl": // Developer is responsible of calling revokeObjectURL if (typeof globalObject.URL !== "undefined" && typeof globalObject.URL.createObjectURL === "function") { return globalObject.URL && globalObject.URL.createObjectURL(getBlob(buildDocument())) || void 0; } else { console.warn("bloburl is not supported by your system, because URL.createObjectURL is not supported by your browser."); } break; case "datauristring": case "dataurlstring": var dataURI = ""; var pdfDocument = buildDocument(); try { dataURI = btoa(pdfDocument); } catch (e) { dataURI = btoa(unescape(encodeURIComponent(pdfDocument))); } return "data:application/pdf;filename=" + options.filename + ";base64," + dataURI; case "pdfobjectnewwindow": if (Object.prototype.toString.call(globalObject) === "[object Window]") { var pdfObjectUrl = "https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js"; var integrity = ' integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==" crossorigin="anonymous"'; if (options.pdfObjectUrl) { pdfObjectUrl = options.pdfObjectUrl; integrity = ""; } var htmlForNewWindow = "" + '"; var nW = globalObject.open(); if (nW !== null) { nW.document.write(htmlForNewWindow); } return nW; } else { throw new Error("The option pdfobjectnewwindow just works in a browser-environment."); } case "pdfjsnewwindow": if (Object.prototype.toString.call(globalObject) === "[object Window]") { var pdfJsUrl = options.pdfJsUrl || "./examples/PDF.js/web/viewer.html"; var htmlForPDFjsNewWindow = "" + "" + '' + ""; var dataURLNewWindow = globalObject.open(); if (dataURLNewWindow !== null) { dataURLNewWindow.document.write(htmlForDataURLNewWindow); dataURLNewWindow.document.title = options.filename; } if (dataURLNewWindow || typeof safari === "undefined") return dataURLNewWindow; } else { throw new Error("The option dataurlnewwindow just works in a browser-environment."); } break; case "datauri": case "dataurl": return globalObject.document.location.href = this.output("datauristring", options); default: return null; } });

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

var link = document.getElementById('linklink');
link.target = '_blank';
//link.href = window.URL.createObjectURL(convertBase64UrlToBlob(pdf.output('datauristring',{filename: 'A4.pdf'})));//140ms Base64 数据转 Blob
//link.href = window.URL.createObjectURL(pdf.output('blob',{filename: 'A4.pdf'}));//77ms Base64 数据
link.href = pdf.output('bloburi');//77ms 直接输出Blob 链接
//link.href = 
//pdf.output('pdfobjectnewwindow',{filename: 'A41.pdf'});//弹出对象窗口,无用
//link.href = pdf.output('dataurl',{filename: 'A4.pdf'});//数据链接无用				
//pdf.output('pdfjsnewwindow',{filename: 'A42.pdf'});//弹出窗口
//pdf.output('dataurlnewwindow',{filename: 'A43.pdf'});//弹出窗口,无用
link.download ="A41.pdf";				
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 :




    
    

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

WebView mWebView;

onCreate 中赋值 mWebView:

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

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

@SuppressLint("SetJavaScriptEnabled")
	private void setWebStyle() {  
    	WebSettings  webseting  =  mWebView.getSettings();  
        
        
        webseting.setAppCachePath(getApplicationContext().getCacheDir().getAbsolutePath());
        webseting.setUseWideViewPort(true);
        webseting.setLoadWithOverviewMode(true);
        //webseting.setPluginState(WebSettings.PluginState.ON);        
        webseting.setDomStorageEnabled(true);//最重要的方法,一定要设置,这就是出不来的主要原因  //webseting.setDomStorageEnabled(true);
        
        webseting.setSupportZoom(true);  
        webseting.setDefaultTextEncodingName("utf-8");
        /* 下载blob准备 */        
		webseting.setJavaScriptEnabled(true);
		webseting.setJavaScriptCanOpenWindowsAutomatically(true);

        /***********************/
        mWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);// 去掉底部和右边的滚动条
        mWebView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);  // 去掉底部和右边的滚动条
        
        mWebView.requestFocus();  
      
         webseting.setCacheMode(WebSettings.LOAD_DEFAULT);   // 默认使用缓存 
        // webseting.setCacheMode(WebSettings.LOAD_NO_CACHE); //默认不使用缓存!
         webseting.setAppCacheMaxSize(1024*1024*20);//设置缓冲大小,便于第一次缓存字体,否则每次下载字体需时太长。 
         String  appCacheDir=this.getApplicationContext().getDir("cache",Context.MODE_PRIVATE).getPath();       
                 webseting.setAppCachePath(appCacheDir);   
                 webseting.setAllowFileAccess(true);   
                 webseting.setAppCacheEnabled(true);   
                 webseting.setCacheMode(WebSettings.LOAD_DEFAULT|WebSettings.LOAD_CACHE_ELSE_NETWORK);
          
      //webview 动作重写 
        mWebView.setWebViewClient(new WebViewClient(){
            @Override
			public boolean shouldOverrideUrlLoading(WebView view, String url) {      
                view.loadUrl(url);   
                return false;// false 显示frameset, true 不显示Frameset ,内嵌页面
                //return true;      
            }     
            @Override    
            public void onPageStarted(WebView view, String url, Bitmap favicon) {    
                //有页面跳转时被回调             	
            	//dialog = ProgressDialog.show(webxj.this,null,"数据加载中,请稍侯...");              	
            	super.onPageStarted(view, url,favicon);  
            }              
            @Override
			public void onPageFinished(WebView view, String url) {   
            	//页面跳转结束后被回调  
            	//dialog.dismiss();
                super.onPageFinished(view, url);   
            }   
            @Override    
            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {    
            	 super.onReceivedError(view, errorCode, description, failingUrl);//错误处理+ description
                Toast.makeText(webxj.this, "错误:请检查网络链接! " , Toast.LENGTH_SHORT).show();  
                dialog.dismiss(); 
		         new Handler().postDelayed(new Runnable() {  
			            public void run() {  
			            	mWebView.loadUrl(urlw);    //显示等待画面
			            }        
			        }, 500);                 
            }
            
        });  
        //禁用webview右键功能:长按
        mWebView.setOnLongClickListener(new OnLongClickListener(){
        	public boolean onLongClick(View v) {
				// TODO Auto-generated method stub
				return true;
			}
        });

        //blob
        //下载支持,A 标签内需 download
        mWebView.setDownloadListener(new MyWebViewDownLoadListener());    //  设置 WebView 下载监听器  
        mWebView.addJavascriptInterface(mDownloadBlobFileJSInterface, "Android"); // Blob 下载 js 定义
		mDownloadBlobFileJSInterface.setDownloadGifSuccessListener(new openDownloadfile()); // Blob 下载完成后监听器
     } 

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

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

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

参考资料 1 中的:

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

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

/* 定义下载完成事件监听器 */
private class openDownloadfile implements DownloadGifSuccessListener {    	
	public void ondownloadGifSuccess(String gifFile){
		String fileName=gifFile.substring(gifFile.lastIndexOf("/")+1); 
		String directory=gifFile.substring(0,gifFile.lastIndexOf("/")+1);
		// System.out.println("11.下载成功,fileName:"+fileName);
		// System.out.println("12.下载成功,directory:"+directory);
		
        File File = new File(directory,fileName);		
		Intent intent = getFileIntent(File); 
        startActivity(intent); //调用万部程序打开
		// System.out.println("13.下载成功,在Download目录下:"+gifFile);
	}  
}

public Intent getFileIntent(File file){  
//       Uri uri = Uri.parse("http://m.ql18.com.cn/hpf10/1.pdf");  
        Uri uri = Uri.fromFile(file);  
        String type = getMIMEType(file);  
        // Log.e("tag", "type="+type);  
        Intent intent = new Intent("android.intent.action.VIEW");  
        intent.addCategory("android.intent.category.DEFAULT");  
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
        intent.setDataAndType(uri, type);  
        return intent;  
      } 

private String getMIMEType(File f){     
      String type="";    
      String fName=f.getName();    
      // * 取得扩展名 * /    
      String end=fName.substring(fName.lastIndexOf(".")+1,fName.length());//.toLowerCase();
      Locale loc = Locale.getDefault();//可以去掉这步
      end=end.toLowerCase(loc);
      // * 依扩展名的类型决定MimeType * /  
      if(end.equals("pdf")){  
          type = "application/pdf";//  
      }  
      else if(end.equals("m4a")||end.equals("mp3")||end.equals("mid")||    
      end.equals("xmf")||end.equals("ogg")||end.equals("wav")){    
        type = "audio/*";     
      }    
      else if(end.equals("3gp")||end.equals("mp4")){    
        type = "video/*";    
      }    
      else if(end.equals("jpg")||end.equals("gif")||end.equals("png")||    
      end.equals("jpeg")||end.equals("bmp")){    
        type = "image/*";    
      }    
      else if(end.equals("apk")){     
        // * android.permission.INSTALL_PACKAGES * /     
        type = "application/vnd.android.package-archive";   
      }  
//      else if(end.equals("pptx")||end.equals("ppt")){  
//        type = "application/vnd.ms-powerpoint";   
//      }else if(end.equals("docx")||end.equals("doc")){  
//        type = "application/vnd.ms-word";  
//      }else if(end.equals("xlsx")||end.equals("xls")){  
//        type = "application/vnd.ms-excel";  
//      }  
      else{  
//        // * 如果无法直接打开,就跳出软件列表给用户选择 * /    
        type="*/*"; //因注释多加了一个空格 
      }  
      return type;  
    }  

其中 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 文件名:

    // 改装blob下载 
  private class MyWebViewDownLoadListener implements DownloadListener {    	  
		public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype,long contentLength){  
			if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
				Toast.makeText(webxj.this, "需要SD卡。",  Toast.LENGTH_SHORT).show();
				return;  
			}			
			//Toast.makeText(webxj.this,"Url:"+url.lastIndexOf("data:app"), Toast.LENGTH_SHORT).show();
			// System.out.println("Url:"+url);
			// Log.e("tag", "Url="+url);			
			if(url.indexOf("blob:http")==0){
				urlP=url;
				// System.out.println("打开:"+urlP);
				//mWebView.loadUrl("javascript:calljs();");				
				mWebView.evaluateJavascript("javascript:getDname('"+urlP+"')", new ValueCallback(){
					public void onReceiveValue(String value){						
						// System.out.println("JS返回::"+value);
						pdffn=value.replace("\"","");
						// System.out.println("pdffn:"+pdffn);
						File directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);  
						File file=new File(directory,pdffn);  
						if(file.exists()){  
							// Log.e("tag", "The file has already exists.");  //文件已存在
							System.out.println("文件已存在:"+directory+pdffn);
							Toast.makeText(webxj.this, "文件已保存:"+directory+"/"+pdffn,Toast.LENGTH_LONG).show();
					        File File = new File(directory,pdffn);		
							Intent intent = getFileIntent(File); 
					        startActivity(intent);
						}else{ 						
							mWebView.loadUrl(DownloadBlobFileJSInterface.getBase64StringFromBlobUrl(urlP));
						}
					}
				});				
	            //mWebView.loadUrl(DownloadBlobFileJSInterface.getBase64StringFromBlobUrl(url));	            			
			}else{
				DownloaderTask task=new DownloaderTask();  
				task.execute(url); 				
			}
		}  
    }

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

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

dll.rar

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

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

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

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

// 定义于 extends Activity 与 onCreate 之间
	WebView mWebView;    // WebView 实例变量
	private String urlx=new String(""); //存放调用首页的参数等,如 double rndX=Math.random();       urlx="?v="+String.valueOf(rndX);

	private String urlw=new String("file:///android_asset/index.html"); //存放等待页面url
	private String urlm;//存放首页菜单的页面

	DownloadBlobFileJSInterface mDownloadBlobFileJSInterface = new DownloadBlobFileJSInterface(this);    // Blob 下载 js 接口定义
	private String urlP=new String("");    // 下载链接全局变量
	private static String pdffn=new String("");    // Blob 文件名全局变量

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

/**
		 * 转换成file
		 * @param base64
		 */
		private void convertToGifAndProcess(String base64) {
			String fileName =pdffn;// UUID.randomUUID().toString() + ".pdf";
			// System.out.println("3.convertToGifAndProcess:fileName:" + fileName);
			File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
			File gifFile = new File(directory, fileName);
			// System.out.println("4.convertToGifAndProcess:gifFile:" + gifFile);
			// Log.e("tag", "type=convertToGifAndProcess" + gifFile);
			saveFileToPath(base64, gifFile);
			// System.out.println("7.convertToGifAndProcess:mDownloadGifSuccessListener:" + mDownloadGifSuccessListener);
			// System.out.println("8.convertToGifAndProcess gifFile:" + gifFile);
			if (mDownloadGifSuccessListener != null) {
				// System.out.println("10.mDownloadGifSuccessListener 不为空到这里:" + mDownloadGifSuccessListener);
				mDownloadGifSuccessListener.ondownloadGifSuccess(gifFile.getAbsolutePath());
			}
		}

		/**
		 * 保存文件
		 * @param base64
		 * @param gifFilePath
		 */
		private void saveFileToPath(String base64, File gifFilePath) {
			// System.out.println("5.saveFileToPath gifFilePath.getAbsolutePath():" + gifFilePath.getAbsolutePath());
			try {
				byte[] fileBytes = Base64.decode(base64.replaceFirst(
					"data:application/pdf;base64,", ""), 0);
				FileOutputStream os = new FileOutputStream(gifFilePath, false);
				os.write(fileBytes);
				os.flush();
				os.close();
				Toast.makeText(mContext, "文件已保存:"+gifFilePath,Toast.LENGTH_LONG).show();
				// Log.e("tag", "type=saveFileToPath" + gifFilePath);
				// System.out.println("6.saveFileToPath FileOutputStream gifFilePath:" + gifFilePath);

			} catch (Exception e) {
				e.printStackTrace();
			}
		}

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

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

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

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

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

此记!

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

你可能感兴趣的