WebViewで「ファイルのダウンロード」(Blob形式)のデータを取得する [Android]
目次
1. 作るもの
2. 初期設定
3. 画面設計
4. リソース(/res/raw)
5. コーディング
1. 作るもの
WebViewで表示しているHTML(JavaScript)で作成したBlob形式の「ファイルのダウンロード」のデータをAndroid(Java)側に渡します。
Android側ではそのデータを元にファイルを/DownLoadに作成します。
本来はonDownloadStartイベントの第一引数の「url」で生データを取得できます。しかし、今回はurlがBlob形式なので特殊となります。
Blob形式から生データを取得するには「Blob形式のurl」をJavaScriptのAJAXで実行してBASE64形式の長い文字列を取得します。その長い文字列をAndroid(Java)側で受け取りバイナリに変換してPNGファイルをDownLoadフォルダに作成しています。
2. 初期設定
2-1. APIレベル
今回はWebViewコントロールを扱うのでAndroidの動作可能なAPIレベルは21(Android5.0/Lollipop)にしています。
設定方法はAndroid Studioで左のTreeViewにあるGradle Scriptsのbuild.gradleの「minSdkVersionを21」にします。
2-2. 権限
[AndroidManifest.xml]
ファイルを扱うので次のストレージ権限を追加します。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
3. 画面設計
WebViewを1つ配置します。
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <WebView android:id="@+id/webView" android:layout_width="368dp" android:layout_height="447dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>
4. リソース(/res/raw)
今回のリソースはindex.htmlとtest.jpgです。両方ともresフォルダの中にrawフォルダを作成してその中に入れてください。尚、プロジェクトにrawのリソースを追加する方法はWebViewの基本操作をご覧下さい。
[index.html]
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script> // キャンバス var src_canvas; var src_ctx; window.onload = function(){ // DOMを変数に格納するとアクセスが早くなる src_canvas = document.getElementById("SrcCanvas"); src_ctx = src_canvas.getContext("2d"); var image = document.getElementById("img_source"); // 画像の設定 image.src = "test.jpg"; image.onload = function (){ src_canvas.width = image.width; src_canvas.height = image.height; // キャンバスに画像を描画 src_ctx.drawImage(image,0,0); } image.onerror = function (){ alert('この画像は読み込めません。'); }; } // 画像をBlob形式でダウンロード function run(){ var F = new TFileStream(); var png = src_canvas.toDataURL().replace("data:image/png;base64,",""); png = window.atob(png); F.WriteString(png); F.SaveToFile('dummy.png','image/png'); } //////////////////////////////////////////////////////////////////////////////// // 汎用クラス //////////////////////////////////////////////////////////////////////////////// // --------------------- // TFileStream // --------------------- function TFileStream(BufferSize) { if (BufferSize == undefined) this.MemorySize = 5000000; // 5M else this.MemorySize = parseInt(BufferSize, 10); this.Size = 0; this.Stream = new Uint8Array(this.MemorySize); } // --------------------- // TFileStream.Method // --------------------- TFileStream.prototype = { _AsciiToUint8Array: function (S) { var len = S.length; var P = new Uint8Array(len); for (var i = 0; i < len; i++) { P[i] = S[i].charCodeAt(0); } return P; }, WriteByte: function (value) { var P = new Uint8Array(1); P[0] = value; this.WriteStream(P); }, WriteWord: function (value) { var P = new Uint8Array(2); P[1] = (value & 0xFF00) >> 8; P[0] = (value & 0x00FF); this.WriteStream(P); }, WriteDWord: function (value) { var P = new Uint8Array(4); P[3] = (value & 0xFF000000) >> 24; P[2] = (value & 0x00FF0000) >> 16; P[1] = (value & 0x0000FF00) >> 8; P[0] = (value & 0x000000FF); this.WriteStream(P); }, WriteWord_Big: function (value) { var P = new Uint8Array(2); P[1] = (value & 0x00FF); P[0] = (value & 0xFF00) >> 8; this.WriteStream(P); }, WriteDWord_Big: function (value) { var P = new Uint8Array(4); P[3] = (value & 0x000000FF) P[2] = (value & 0x0000FF00) >> 8; P[1] = (value & 0x00FF0000) >> 16; P[0] = (value & 0xFF000000) >> 24;; this.WriteStream(P); }, WriteString: function (S) { var P = this._AsciiToUint8Array(S); // メモリの再編成 if (this.Stream.length <= (this.Size + P.length)) { var B = new Uint8Array(this.Stream); this.Stream = new Uint8Array(this.Size + P.length + this.MemorySize); this.Stream.set(B.subarray(0, B.length)); } this.Stream.set(P, this.Size); this.Size = this.Size + P.length; }, WriteStream: function (AStream) { // メモリの再編成 if (this.Stream.length <= (this.Size + AStream.length)) { var B = new Uint8Array(this.Stream); this.Stream = new Uint8Array(this.Size + AStream.length + this.MemorySize); this.Stream.set(B.subarray(0, B.length)); } this.Stream.set(AStream, this.Size); this.Size = this.Size + AStream.length; }, getFileSize: function () { return this.Size; }, SaveToFile: function (FileName,type) { if (window.navigator.msSaveBlob) { window.navigator.msSaveBlob(new Blob([this.Stream.subarray(0, this.Size)], { type: type }), FileName); } else { var a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([this.Stream.subarray(0, this.Size)], { type: type })); //a.target = '_blank'; a.download = FileName; document.body.appendChild(a); // FF specification a.click(); document.body.removeChild(a); // FF specification } }, } </script> </head> <body> <br> <input type="button" onclick="run();" value="実行する"><br> <br> <canvas id="SrcCanvas" ></canvas> <img id="img_source" style="display:none;"> </body> </html>
[test.jpg]
各自で用意してください。
5. コーディング
import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.media.MediaScannerConnection; import android.os.Environment; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Base64; import android.webkit.DownloadListener; import android.webkit.JavascriptInterface; import android.webkit.WebView; import android.widget.Toast; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.sql.Date; import java.text.SimpleDateFormat; public class MainActivity extends AppCompatActivity { //////////////////////////////////////////////////////////////////////////////////////////////////// // 汎用メソッド //////////////////////////////////////////////////////////////////////////////////////////////////// // パーミッションダイアログ public boolean CheckPermission(Activity actibity, String permission, int requestCode){ // 権限の確認 if (ActivityCompat.checkSelfPermission(actibity, permission) != PackageManager.PERMISSION_GRANTED) { // 権限の許可を求めるダイアログを表示する ActivityCompat.requestPermissions(actibity, new String[]{permission},requestCode); return false; } return true; } // リソースからファイルを生成する(/data/data/パッケージ名/files/) public boolean setRawResources(Context context , int resourcesID, String fileName){ boolean result = false; // リソースの読み込み InputStream is = context.getResources().openRawResource(resourcesID); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte [] buffer = new byte[1024]; try{ // 1024バイト毎、ファイルを読み込む while(true) { int len = is.read(buffer); if(len < 0) break; baos.write(buffer, 0, len); } }catch (Exception e){ e.printStackTrace(); return result; } // ファイルの生成 File file = new File(context.getFilesDir() + "/" + fileName); FileOutputStream fos = null; try { fos = new FileOutputStream(file); fos.write(baos.toByteArray()); result = true; } catch (Exception e) { e.printStackTrace(); } finally { if(fos != null){ try{ fos.close(); }catch (Exception e){ e.printStackTrace(); } } } return result; } // 現在日時の取得 public String getSysdate() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); return sdf.format(new Date(System.currentTimeMillis())); } //////////////////////////////////////////////////////////////////////////////////////////////////// // 内部用クラス //////////////////////////////////////////////////////////////////////////////////////////////////// // JavaScriptInterface // ※JavaScriptからこのクラスのメソッドを呼び出す private class JavaScriptInterface { @JavascriptInterface public void base64ToFile(String base64) { // Downloadフォルダへファイルを作成する String fullPath = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS) + "/" + getSysdate() + ".png"; final File file = new File(fullPath); // Base64の長い文字列をバイナリに変換して保存する byte[] pByteArray = Base64.decode(base64.replaceFirst("^data:image/png;base64,", ""), 0); FileOutputStream fos; try { fos = new FileOutputStream(fullPath, false); fos.write(pByteArray); fos.close(); } catch (Exception e) { e.printStackTrace(); } // Androidにファイルを認識させる MediaScannerConnection.scanFile(MainActivity.this, new String[]{fullPath}, null, null); Toast.makeText(MainActivity.this,"DownloadフォルダにPNGファイルを生成しました。", Toast.LENGTH_SHORT).show(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // イベント //////////////////////////////////////////////////////////////////////////////////////////////////// @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // *************** // 権限設定 // *************** // ストレージの権限確認 CheckPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE, 1000); // *************** // リソース // *************** // リソースからファイルを生成する(/data/data/パッケージ名/files/に作成) setRawResources(MainActivity.this, R.raw.index, "index.html"); setRawResources(MainActivity.this, R.raw.test, "test.jpg"); // *************** // WebView // *************** final WebView webView = findViewById(R.id.webView); // キャッシュクリア // ※開発時のみ有効にする webView.clearCache(true); // JavaScriptを有効にする webView.getSettings().setJavaScriptEnabled(true); // JavaScriptInterfaceの初期設定 webView.addJavascriptInterface(new JavaScriptInterface(), "android"); // test.jpgを読み込めるようにする // ※コレを設定しないと次のエラーになります。 // ※Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported webView.getSettings().setAllowFileAccessFromFileURLs(true); // ダウンロードイベントをオーバーライドする // ※JavaScriptのrun()で作成したファイルのダウンロード webView.setDownloadListener(new DownloadListener() { @Override public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) { // 第一引数の「url」は「BlobのURL」です。 // AJAXで「BlobのURL」(blob:file:///xxx形式)からデータを読み込み、 // base64データをAndroid側のbase64ToFileメソッドに渡しています。 String js = "javascript:" + " var xhr = new XMLHttpRequest();" + " xhr.open('GET', '" + url + "', true);" + " xhr.setRequestHeader('Content-type','image/png');" + " xhr.responseType = 'blob';" + " xhr.onload = function(e) {" + " if (this.status == 200) {" + " var blobUrl = this.response;" + " var reader = new FileReader();" + " reader.readAsDataURL(blobUrl);" + " reader.onloadend = function() {" + " base64data = reader.result;" + " android.base64ToFile(base64data);" + " }" + " }" + "};" + "xhr.send();"; // 上記のJavaScriptを実行する webView.loadUrl(js); } }); // ファイルを読み込む webView.loadUrl("file:///" + MainActivity.this.getFilesDir() + "/index.html"); } }
今回のコードは次の参考URLを参考にしています。感謝です。
参考URL
Download Blob file from Website inside Android WebViewClient
関連記事
前の記事: | WebViewの<input type="file">でカメラ、画像からデータを取得する [Android] |
次の記事: | WebViewの<input type="file" multiple>でファイルを複数選択する [Android] |