I'm developing an application in React Native which uses some windows native components. One of those components is a WebView.
I'm showing a website via this WebView and this website records the camera video and prompts the user to download the file (it's a file created from a Blob and URL.createObjectURL(blob)). However, the WebView does not open any folder picker or file picker, and the user cannot save the file.
Here is my WebView code (remember it has some properties of React Native):
using ReactNative.UIManager;
using ReactNative.UIManager.Annotations;
using System;
using System.Reflection;
using Windows.UI.Xaml.Controls;
namespace ListProject.CustomModules
{
public class PremissionWebviewViewManager : SimpleViewManager<WebView>
{
private Uri sourceUri;
private WebView thiswv;
public override string Name
{
get
{
return "PermissionWebviewViewManager";
}
}
[ReactProp("sourceUri")]
public void SetSource(WebView view, String source)
{
this.sourceUri = new Uri(source);
thiswv.Navigate(this.sourceUri);
}
protected override WebView CreateViewInstance(ThemedReactContext reactContext)
{
thiswv = new WebView();
thiswv.PermissionRequested += HandlePermissionRequested;
// thiswv.UnviewableContentIdentified += HandleDownloadContent; <--- I thought this could help, but I came up with nothing
return thiswv;
}
private void HandlePermissionRequested(WebView sender, WebViewPermissionRequestedEventArgs args)
{
args.PermissionRequest.Allow();
}
private void HandleDownloadContent(WebView sender, WebViewUnviewableContentIdentifiedEventArgs args)
{
// Windows.System.Launcher.LaunchUriAsync(args.Uri);
}
}
}
Here is the React Native component:
render() {
return (
<PermissionWebview
style={{flex: 1}}
mediaPlaybackRequiresUserAction={false}
domStorageEnabled={true}
allowsInlineMediaPlayback={true}
source={{uri: this.website}}
sourceUri={this.website}
allowFileAccessFromFileURLs={true}
allowUniversalAccessFromFileURLs={true}
/>
);
}
And here the script used in JS on the website part to trigger the download:
var blob = new Blob(recordedVideo, {type: 'video/webm'});
var videoUrl = URL.createObjectURL(blob);
var anchorTag = $('<a class="download_link"></a>');
anchorTag.attr('href', videoUrl);
anchorTag.attr('target', '_blank');
anchorTag.attr('download', 'video.webm');
// Add the anchor tag to DOM
$('body').append(anchorTag);
// We use DOM element and not JQ element to trigger DOM events (not JQ ones)
anchorTag[0].click();
// Remove tag once clicked and event triggered
anchorTag.remove();
The Windows app has all file capabilites granted in the manifest (though the user is not asked to let the app access the file system), and in a native Edge browser it works - so it's not an Edge API limitation (WebView uses Edge engine in UWP W10).
How can I achieve so the download window is triggered and the file can be downloaded on the client?
Thank you.
Related
I'm developing an angular website which is loading in an app from a WebView, and there is only one of the links in it that has to be opened outside of the app (external browser)
I need a way to handle this from JavaScript not putting extra work to the android side.
and i have already tried some ways including:
window.open("url","_system")
(navigator as any).app.loadUrl("http://google.com", {openExternal : true});
Well, there is no such thing
instead it must be handled from android application code. you can add a parameter to the url when u need it to open in external browser, ( here it is external=true ) and then check for that parameter in your webview url loading as below:
webView.setWebViewClient(new WebViewClient(){
#RequiresApi(api = Build.VERSION_CODES.N)
#Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
if((String.valueOf(request.getUrl())).contains("external=true")) {
Intent intent = new Intent(Intent.ACTION_VIEW, request.getUrl());
view.getContext().startActivity(intent);
return true;
} else {
view.loadUrl(String.valueOf(request.getUrl()));
}
return true;
}
});
My requirements:
This should work in Android and iOS
I want to load a script (could be Javascript or Lua or any other scripting language) from a file (not necessarily over the network)
The script should run in a background thread and be able to post to the UI thread
I want to use the same script in iOS and Android, so that I can reuse the script code both in Android and iOS app
What are my options?
WebView Android with Javascript Enabled
SFWebView i think in iOS also support Javascript.
Not sure that you can use it in background thread as Android Webview will only runs on UI thread
Answer:
1. You MUST load the HTML into string:
private String readHtml(String remoteUrl) {
String out = "";
BufferedReader in = null;
try {
URL url = new URL(remoteUrl);
in = new BufferedReader(new InputStreamReader(url.openStream()));
String str;
while ((str = in.readLine()) != null) {
out += str;
}
} catch (MalformedURLException e) {
} catch (IOException e) {
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return out;
}
Load WebView with base URL:
String html = readHtml("Your Web URL");
mWebView.loadDataWithBaseURL("file:///android_asset/", html, "text/html", "utf-8", "");
In this particular case you should have all .js files you want to use on the page to reside somewhere under "assets" folder of project. For example:
/MyProject/assets/jquery.min.js
In your remote html page you have to load .js and .css files that reside in your application like:
the same applies to all other local resources like images, etc. their path has to start with
file:///android_asset/
A WebView would first load the raw HTML you have provided as string, then pick .js, .css and other local resourses and then would load remote content.
In android, While loading the page into webview. Intercept the script call.
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if ( url.endsWith("script.js") ) {
AssetFileDescriptor fileDescriptor = assetManager.openFd("script.js");
FileInputStream stream = fileDescriptor.createInputStream();
return new WebResourceResponse("text/javascript","UTF-8",stream);
}
}
Webview runs on ui thread. I am not sure that there is way to load webresources from background threads.
I am trying to inject a javascript namespace with function from App to Android WebView
NOTE: I do not have any control on the actual source being loaded on WebView apart from the below script. I am working in Xamarin so the Android code is C#
So there are two major components that I am using:
(function() {
console.log("Loading JS Bindings");
function addOnClickHandler () {
$(document).ready(function () {
if (window.InjectedNamespace) { // check InjectedNamespace namespace exists
var onClickHandler = window.InjectedNamespace.handleOnClick;
if (typeof onClickHandler === 'function') {
$(":button, a").each(function (idx) {
$(this).click(function() {
onClickHandler("click!");
});
});
} else {
console.error('missing onclick handler');
}
} else {
console.error('missing InjectedNamespace namespace');
}
});
}
})();
Above script which will be added by the third-party, which will attach click function to all <button> and <a> tags.
Below namespace will be injected from the App side
var InjectedNamespace={handleOnClick:function(a){Internal.handleOnClickEvent(a)}};
On App side I have this Javascript interface object
class JSInterfaceObject : Java.Lang.Object
{
readonly CustomWebView View;
string Data;
public JSInterfaceObject(CustomWebView view)
{
View = view;
}
[Export]
[JavascriptInterface]
// only methods exposed with annotation [JavascriptInterface] are exposed to javascript
public void handleOnClickEvent(string data)
{
Log.ForContext("TAG", TAG).Debug("handleOnClickEvent: " + data);
if(View.OnClick != null)
{
View.OnClick(data);
}
}
}
And this interface is added to webview using
AddJavascriptInterface(new JSInterfaceObject(this), "Internal");
And in OnPageStarted I am injecting the javascript using
webview.LoadUrl("javascript:"+"var IM={handleOnClick:function(a){Internal.handleOnClickEvent(a)}};");
This setup work fine for Android SDK < 24. But for Webview in SDK >=24 the script always errors out with missing InjectedNamespace namespace, which implies the LoadUrl in OnPageStarted failed!
This check is done in $(document).ready in the script.
I found this note in "Android 7.0 for Developers" which says
"Starting with apps targeting Android 7.0, the Javascript context will
be reset when a new page is loaded. Currently, the context is carried
over for the first page loaded in a new WebView instance.
Developers looking to inject Javascript into the WebView should
execute the script after the page has started to load."
So tried to add the javascript injection code after the page started loading but got the same error. Also tried using the WebView.EvaluateJavascript() but error persists.
The error disappers if I change my TargetSDK to <=23.
Windows won't let my WebView_ScriptNotify Event receive a call if the html is loaded from ms-appdata.
I'm aware that I can use the ms-appx-web protocol to load such a file from my app bundle, but this is no option because the data to show are downloaded after install of the app.
I also can't just use webView.navigateToString because this won't include the referenced libraries in the html file.
Currently I'm trying something like this in my Class.xaml.cs
WebView webView = new WebView();
webView.ScriptNotify += WebView_ScriptNotify;
Uri navigationUri = new Uri(#"ms-appdata:///local/index.html");
webView.Navigate(navigationUri);
and
private void WebView_ScriptNotify(object sender, NotifyEventArgs e)
{
System.Diagnostics.Debug.WriteLine("ScriptNotifyValue: " + e.Value);
//I want to do the magic here, but this will never be called
}
in the html file is
<div id="content">
<div class="btn" onClick="window.external.notify('hello world');"</div>
</div>
Furthermore, it's no option to use InvokeScript(), because I don't know when the event must be fired and the values for it.
Yet it's mandatory to use files from ms-appdata.
Do you know a solution for this?
Even an alternative workaroung would amaze me.
Ref Script notify changes in XAML:
For content to be able to send notifications the following conditions apply:
The source of the page should be from the local system via NavigateToString(), NavigateToStream() or ms-appx-web:///
Or
The source of the page is delivered via https:// and the site domain name is listed in the app content URI’s section of the package manifest.
So to solve this issue, we can use WebView.NavigateToLocalStreamUri method with the protocol ms-local-stream://, rather than ms-appdata://. For example:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// The 'Host' part of the URI for the ms-local-stream protocol needs to be a combination of the package name
// and an application-defined key, which identifies the specific resolver, in this case 'MyTag'.
Uri url = webView.BuildLocalStreamUri("MyTag", "index.html");
StreamUriWinRTResolver myResolver = new StreamUriWinRTResolver();
// Pass the resolver object to the navigate call.
webView.NavigateToLocalStreamUri(url, myResolver);
}
private void webView_ScriptNotify(object sender, NotifyEventArgs e)
{
System.Diagnostics.Debug.WriteLine("ScriptNotifyValue: " + e.Value);
}
}
public sealed class StreamUriWinRTResolver : IUriToStreamResolver
{
public IAsyncOperation<IInputStream> UriToStreamAsync(Uri uri)
{
if (uri == null)
{
throw new Exception();
}
string path = uri.AbsolutePath;
// Because of the signature of the this method, it can't use await, so we
// call into a seperate helper method that can use the C# await pattern.
return GetContent(path).AsAsyncOperation();
}
private async Task<IInputStream> GetContent(string path)
{
// We use app's local folder as the source
try
{
Uri localUri = new Uri("ms-appdata:///local" + path);
StorageFile f = await StorageFile.GetFileFromApplicationUriAsync(localUri);
IRandomAccessStream stream = await f.OpenAsync(FileAccessMode.Read);
return stream;
}
catch (Exception) { throw new Exception("Invalid path"); }
}
}
For more info, please see Remarks and Examples in WebView.NavigateToLocalStreamUri method and also Custom URI resolving in What’s new in WebView in Windows 8.1. Besides, there is also a WebView control (XAML) sample on GitHub.
I'm currently having trouble to nagivate to local file which have the index.html.
I had try to copied the URL link to firefox and google chrome and it worked. But when I'm trying to navigate to the URL link with C# Web Browser Control, it gave me a black screen with
Fatal: loading error (Security Error)
which I feel very weird.
Here is the example link: file://127.0.0.1/c$/Users/Marcus/Desktop/03%20Virtual%20Tour/dataranlang/index.html
Here is my code that used to called for the URL link in C# Web Browser Control:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Reflection;
namespace Perak360
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.WB.Loaded += (s, e) =>
{
// get the underlying WebBrowser ActiveX object;
// this code depends on SHDocVw.dll COM interop assembly,
// generate SHDocVw.dll: "tlbimp.exe ieframe.dll",
// and add as a reference to the project
var activeX = this.WB.GetType().InvokeMember("ActiveXInstance",
BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
null, this.WB, new object[] { }) as SHDocVw.WebBrowser;
// now we can handle previously inaccessible WB events
activeX.FileDownload += activeX_FileDownload;
};
this.Loaded += (s, e) =>
{
try
{
this.WB.Source = new Uri(new System.IO.FileInfo(#"C:\Users\Marcus Tan\Documents\Visual Studio 2013\Projects\Perak360\Perak360\Perakin360 Offline (Final)\index.html").FullName);
//this.WB.Navigate(Properties.Settings.Default.WebBrowserPath);
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
};
}
void activeX_FileDownload(bool ActiveDocument, ref bool Cancel)
{
Cancel = true;
}
private void CloseWindow(object sender, TouchEventArgs e)
{
Application.Current.Shutdown();
}
}
}
I know that if i changed the URL link to file://C:/Users/Marcus%20Tan/Desktop/03%20Virtual%20Tour/dataranlang/index.html it may work, but there will be a javascript prompt out which ask user to enable the blocked content which I don't want it to be appear.
Any clue what wrong with my URL link or code which it gave me a black screen without the loading error screen? I had try to search online for solution and try to set the Internet Security setting, but none of it work.
Please guide me through this.