Android Question Download files from WebView (local storage)

max123

Well-Known Member
Licensed User
Longtime User
Hi all,

I want to explain what I do with my project but do not confuse it with sophysticated things, the question here is just one, how to download a file from a WebView, how to handle it so when I press on the link the file should download.

Suppose you have a page with download link, how to download a file?

I've developed an app that permits to manage threejs javascript 3D library and WebGL.

It is a small JS IDE that works on Android.
It can create simple and very complex scenes.

This app have 2 pages in a TabHost:
- Page1 There is a big EditText inside a ScrollView2D, here I can write HTML + Javascript code to execute. There are options to Save and Load HTML files.
- Page2 There is a WebView, when I press the TabHost page2 the HTML file is saved to DirRootInternal, then loaded inside a WebView where the 3D scene is rendered.

To be more precise I recreated the threejs distribution (just folders and files needed mantaining original structure) inside the app folder (in DirRootExternal) where the library search all files like JS imports, textures, models etc.

Initially I had problem to reference files URL pointing to DirRootExternal but then I added this sub
B4X:
Sub AllowUniversalAccessFromFileURLs(wv As WebView)
    Dim jo As JavaObject = wv
    Dim settings As JavaObject = jo.RunMethod("getSettings", Null)
    Dim r As Reflector
    r.Target = settings
    r.RunMethod2("setAllowUniversalAccessFromFileURLs", True, "java.lang.boolean")
End Sub
and now I'm able to import any file I need, this worked well and I'm really impressed on what this library can do, I've managed to create some simples and complex scenes, import 3D models like STL, OBJ, 3DS, Collada, 3MF, PLY, apply textures, mipmapping etc.

------

Now I've the opposed problem, that library have Exporters, so I can create objects starting from primitives, boxes, spheres, cylinders etc. manage these with boolean operations like subtract, union, intersect, then export the result as OBJ, STL and other 3D file formats.

My goal is to export objects in STL file format so I can import these inside a 3D printing slicing program like RepetierHost, Cura etc., slice and print with my 3D printer.

To start with, this is a simple Javascript code that add to the scene a simple cube (screenshot attached) and then add to the page 2 links, one to export it as STL in ASCII format and one to export to BINARY format.
JavaScript:
<!DOCTYPE html>

<html>

    <head>
        <meta charset="utf-8">
        <title>ThreeJs</title>
        <!-- This is important to get the correct canvas size on mobile devices -->
        <meta name='viewport' content='width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'/>
        <style>
            /* Set margin to 0 and overflow to hidden, to go fullscreen */
            body { margin: 0; overflow: hidden; }
        </style>
    </head>

    <body>
 
        <!-- Import three.js  Note that this can be changed with minified three.min.js -->
        <script src="../build/three.js"></script>

        <!-- [STEP 1] INSERT HERE IMPORTS. HERE A TEMPLATE, JUST EDIT AND/OR REMOVE, COMMENT, UNCOMMENT AS NECESSARY -->

      <script src="js/controls/OrbitControls.js"></script>
      <script src="js/libs/stats.min.js"></script>
      <script src="js/libs/dat.gui.js"></script>    <!-- minified dat.gui.min.js can be used instead -->
 
      <script src="js/exporters/STLExporter.js"></script>  <!-- IMPORT STL EXPORTER -->

 <!-- <script src="js/utils/GeometryUtils.js"></script> -->
 <!-- <script src="js/utils/BufferGeometryUtils.js"></script> -->
 <!-- <script src="js/loaders/STLLoader.js"></script> -->
 
        <script>

  ///// [STEP 2] INSERT HERE JAVASCRIPT CODE /////

            let scene, camera, renderer, exporter, mesh, controls;

            const params = {
                exportASCII: exportASCII,
                exportBinary: exportBinary
            };

            init();
            animate();

            function init() {

                camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 5000 );
                camera.position.set( 200, 100, 200 );

                scene = new THREE.Scene();
                scene.background = new THREE.Color( 0xa0a0a0 );

                exporter = new THREE.STLExporter();  // HERE THE STL EXPORTER

                // Lights

                const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444 );
                hemiLight.position.set( 0, 200, 0 );
                scene.add( hemiLight );

                const directionalLight = new THREE.DirectionalLight( 0xffffff );
                directionalLight.position.set( 0, 200, 100 );
                scene.add( directionalLight );

                // Ground plane

                const ground = new THREE.Mesh( new THREE.PlaneGeometry( 500, 500 ), new THREE.MeshPhongMaterial( { color: 0x0000FF, depthWrite: false } ) );
                ground.rotation.x = - Math.PI / 2;
                scene.add( ground );

                const grid = new THREE.GridHelper( 500, 20, 0x000000, 0x000000 );
                grid.material.opacity = 0.2;
                grid.material.transparent = true;
                scene.add( grid );

                // Export mesh

                const geometry = new THREE.BoxGeometry( 50, 50, 50 );   // THE BOX TO BE EXPORTED, 50x50x50 MILLIMETERS
                const material = new THREE.MeshPhongMaterial( { color: 0x00ff00 } );

                mesh = new THREE.Mesh( geometry, material );
                mesh.position.y = 25;
                scene.add( mesh );

                // WebGL renderer

                renderer = new THREE.WebGLRenderer( { antialias: true } );
                renderer.setPixelRatio( window.devicePixelRatio );   // Adjust with DPI
                renderer.setSize( window.innerWidth, window.innerHeight );
                document.body.appendChild( renderer.domElement );

                // OrbitControls

                controls = new THREE.OrbitControls( camera, renderer.domElement );
                controls.target.set( 0, 25, 0 );
                controls.update();

                // Resize event listener

                window.addEventListener( 'resize', onWindowResize );

                // GUI

                const gui = new dat.GUI();

                gui.add( params, 'exportASCII' ).name( 'Export STL (ASCII)' );
                gui.add( params, 'exportBinary' ).name( 'Export STL (Binary)' );
                gui.open();
            }

            function onWindowResize() {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize( window.innerWidth, window.innerHeight );
            }

            function animate() {
                requestAnimationFrame( animate );
                renderer.render( scene, camera );
                controls.update();
            }

            function exportASCII() {
                Log ("Export ASCII STL file");
                const result = exporter.parse( mesh );
                saveString( result, 'box.stl' );
            }

            function exportBinary() {
                Log ("Export bynary STL file");
                const result = exporter.parse( mesh, { binary: true } );
                saveArrayBuffer( result, 'box.stl' );
            }

            const link = document.createElement( 'a' );  // CREATE DOWNLOAD LINK
            link.style.display = 'none';
            document.body.appendChild( link );

            function save( blob, filename ) {
                Log ("Save the file: " + filename);
                link.href = URL.createObjectURL( blob );
                link.download = filename;
                link.click();
            }

            function saveString( text, filename ) {
                Log ("Save string: " + filename);
                save( new Blob( [ text ], { type: 'text/plain' } ), filename );
            }

            function saveArrayBuffer( buffer, filename ) {
                Log ("Save array buffer: " + filename);
                save( new Blob( [ buffer ], { type: 'application/octet-stream' } ), filename );
            }

            function Log (text) {
               console.log(text);
            }

    //////////// END OF JAVASCRIPT CODE ////////////

        </script>
    </body>
</html>
The problem here is that by clicking on these links nothing happen, threejs library should save files in the root where HTML file is, but I need to manage a WebView to do downloads. I've tried in a lots of ways but without success, probably I missed something basic.

You can see this yourself online on, by clicking on right-bottom you can see html code.
https://threejs.org/examples/misc_exporter_stl.html

Please, can someone help me figure how to do it in the right way ?
Possibly that works on older and newer Android versions so the app can work on older and newer devices.

Many thanks
 

Attachments

  • 20220607_180139.jpg
    20220607_180139.jpg
    107.3 KB · Views: 243
  • 20220607_172501.jpg
    20220607_172501.jpg
    139.1 KB · Views: 234
  • 20220607_172803.jpg
    20220607_172803.jpg
    176.4 KB · Views: 216
  • 20220607_173058.jpg
    20220607_173058.jpg
    298.1 KB · Views: 264
  • 20220607_173616.jpg
    20220607_173616.jpg
    131.3 KB · Views: 224
  • 20220607_173828.jpg
    20220607_173828.jpg
    102.6 KB · Views: 231
  • 20220607_174032.jpg
    20220607_174032.jpg
    139.5 KB · Views: 232
Last edited:

max123

Well-Known Member
Licensed User
Longtime User
Please, some help
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
I can upload a zip file yes, however, you need to download a threejs distribution and copy manually to the app folder,, the project has nothing of special here, I already posted a relevant code (that is html+js), in B4A I just added a WebView from designer and by code I load the html file that I've posted (the same of that online, but have local library access instead of use online cdn that are subject to be changed and updated making the app not working in future).

The project even do some other things because here need to import threejs library, so copy some files from asset unzip a file in app folder and because I wanted make it as a tutorial for threejs library, it copy some demo html files.

The main problem is that threejs dustribution is a very big file, 306 MB, to test it I've removed some unused models, textures etc, but still remain 180 MB, I copy it from asset, this worked but sure not right way.

But this is not relevant with this and maybe can confuse....

Are just download links, nothing of special.

Suppose you have a webpage and a simple download link to download a file, how do you handle it to download ? Only this I need, no other...

As you can see javascript put 2 download links in the page.
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Maybe this post confused some peoples that read it, I wanted to explain what I do with threejs, but this is not anyway relevant.

I already posted a link to the real url, but no problem, I will post it again:
https://threejs.org/examples/misc_exporter_stl.html

The question here is just one, how to download a file from a WebView, how to handle it so when I press on the link the file should download.

Eventually this can be tested with a simple JavaScript that save a simple text file, without use threejs library at all.
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
post a link to the real url so we can see if the links there?
Hi @DonManfred I'm sorry I don't understand what you mean by this.

The link here is local, just javascript handle file stream to be downloaded and webview should download. The first works because the same html file inline can download both 2 files (tested on Firefox) the file is saved to Downloads folder, the second should be handled from B4A, but I do not know how.

I even added a javascript interface to a webview with WebViewExtras, but not sure if this really needed, to handle download I need to call manually by Javascript a B4A sub that handle download with OkHttpUtils2 ?
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
I've tried to call B4A sub DownloadAndSaveFile from javascript and pass to B4A the URL, but it is a blob with this syntax and okHttpUtils2 refuse it:

blob:long hex number

from javascript I've used this:
JavaScript:
function save( blob, filename ) {
   console.log ("Save the file: " + filename);
   link.href = URL.createObjectURL( blob );
   link.download = filename;
   link.click();

   B4A.CallSub("DownloadAndSaveFile", true, link.href);
}
the B4A interface was created with WebViewExtras
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
This question stay unresolved
 
Upvote 0

JohnC

Expert
Licensed User
Longtime User
The below code is some various code from different places, and hopefully it can help in some way:

B4X:
Dim client As JavaObject
client.InitializeNewInstance(Application.PackageName & ".home$MyChromeClient", Null)
nwv = wv
nwv.RunMethod("setWebChromeClient", Array(client))

Dim dl As JavaObject
dl.InitializeNewInstance(Application.PackageName & ".home$MyDownloadListener", Null)
dl.RunMethod("set", Array(nwv))

Sub DownloadListener_Event (MethodName As String, Args() As Object) As Object
    Log("DownloadListener=" & MethodName)
    Return Null
End Sub

Sub onDownloadStart (url As String, userAgent As String, contentDisposition As String, mimeType As String, contentLength As Long)
    Dim FN As String
    Dim I As Intent
   
    Log("url: " & url)
    Log("userAgent: " & userAgent)
    Log("contentDisposition: " & contentDisposition)
    Log("mimeType: " & mimeType)
    Log("contentLength: " & contentLength)
   
    'get filename
    FN = Misc.GetFileName(url)

    'download file
    Wait For (DownloadAndSave(url, Browser.MediaDir,FN)) Complete (Success As Boolean)
    If Success Then
        'launch default app
        i.Initialize(i.ACTION_VIEW, Misc.GetFileUri(Browser.MediaDir, FN))
        i.SetType(mimeType)
        StartActivity(I)
    Else
        ToastMessageShow("Error Downloading file!",True)
    End If
   
End Sub

#if Java
import android.webkit.*;
import android.webkit.WebChromeClient.*;
import android.net.*;
public static void SendResult(Uri uri, ValueCallback<Uri[]> filePathCallback) {
    if (uri != null)
        filePathCallback.onReceiveValue(new Uri[] {uri});
    else
        filePathCallback.onReceiveValue(null);
       
}
public static class MyChromeClient extends WebChromeClient {
@Override
 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
            FileChooserParams fileChooserParams) {
        processBA.raiseEventFromUI(this, "showfile_chooser", filePathCallback, fileChooserParams, fileChooserParams.getAcceptTypes());
        return true;
    }
    }
   
public static class MyDownloadListener implements android.webkit.DownloadListener {
public void set(android.webkit.WebView wv) {
   wv.setDownloadListener(this);
}
 public void onDownloadStart(String url, String userAgent,
                                String contentDisposition, String mimetype,
                                long contentLength)
       {
       processBA.raiseEventFromUI(this, "ondownloadstart", url, userAgent, contentDisposition, mimetype, contentLength);
       }
       }

#End If
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Many thanks @JohnC I will try to manage it. This is an old project but I've to resume it, so I'll take a look on your code.

I remember that I had to use theejs 138 and changed one class to adapt it with an older theejs version to get read working, this way I can open any file like 3D models, textures etc. But only read worked, I cannot save.

The class internally works in a way similar to this.... so at the end download a blob. May I've to extract Blob back to string or binary.
Note that there are two types of STL files, text based and binary. The binary is more suitable, it cannot textually read, but make smaller files by removing spaces and other things.
JavaScript:
var exporter = new THREE.STLExporter();
var str = exporter.parse( scene ); // Export the scene
var blob = new Blob( [str], { type : 'text/plain' } ); // Generate Blob from the string
//saveAs( blob, 'file.stl' ); //Save the Blob to file.stl

//Following code will help you to save the file without FileSaver.js
var link = document.createElement('a');
link.style.display = 'none';
document.body.appendChild(link);
link.href = URL.createObjectURL(blob);
link.download = 'Scene.stl';
link.click();

Here the example I working on to test STL save:
https://threejs.org/examples/misc_exporter_stl
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
After a lots of search and tests, seem there is no clear way to download a Blob on Android WebView.

@JohnC pointed me to the right direction, so Thank You John, your advices with download listener worked very well
and now when I click on the download link, B4X sub is fired. ?

Using the attached code I can finally download a javascript Blob, from the code, result that I had to manage a lot before it worked,
the secrets seem to be:

- add a download listener, because html <a> element is not just a link to another page and it cannot be managed from WebView_OverrideUrl
- when download listener fires, call JavaScript that do an XMLHttpRequest to get back the Blob and extract data
- decode a Blob base64
- remove the blob:contentType from begin of a long resulting string
- removing this will remains only data bytes, pass back data bytes to B4X
- now B4X can save data to a file

Probably this even works with 'data:' scheme that is not much different.

So now I'm able to download Blob, tested with more text and binary files,
the only big problem now is that I cannot get back the file name from response headers,
the content-disposition header should return something like: attachments; filename="box.stl",
but it always return a void string, the header do not exist in the response.

Both content-length and content-type returns the right values, but the response do not contain content-disposition header
or at least I cannot read it no from B4X, no from JS.
I've set a lot of WebView settings, but nothing changed, I cannot get it, but it is somewhere in the response,
any browser can see the URL as blob:https://threejs.org/LongHexNumber and even a file name.

I've read that someone in the world solved this by enabling cookies on the WebView, but I'm not sure of this, I even read that they are enabled by default.

I attached my full test code, for now I manually set the file name in the code, please help me to get it from a response, this is a last step to get it full working.

1720604045028.png
 

Attachments

  • DownloadBlobFromWebView.zip
    21.3 KB · Views: 134
Last edited:
Upvote 0
Top