OK... so I have a Custom HTMLWebview which I want to add Javascript to. A lot of examples I find online have the HTML on Android (and iOS) level but I would prefer keeping the HTML in the Custom Renderer (that way I can fill it with custom content on seperate pages).
Right now I have a button which should fire an event but it is not happening and I'm not sure why.
This is my Custom Renderer:
public class HybridWebView : WebView
{
public HybridWebView()
{
const string html = #"
<html>
<body>
<h3>Test page</h3>
<button type=""button"" onClick=""CSharp.ShowToast('Hello from JS')"">Native Interaction</button>
</body>
</html>";
var htmlSource = new HtmlWebViewSource();
htmlSource.BaseUrl = DependencyService.Get<IBaseUrl>().Get();
htmlSource.Html = html;
Source = htmlSource;
}
}
This is my Android Renderer:
[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace Test.Droid.Renderers
{
public class HybridWebViewRenderer : WebViewRenderer
{
Context _context;
public HybridWebViewRenderer(Context context) : base(context)
{
_context = context;
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (Control != null)
{
Control.Settings.JavaScriptEnabled = true;
Control.AddJavascriptInterface(new HybridJSBridge(Android.App.Application.Context), "CSharp");
}
base.OnElementPropertyChanged(sender, e);
}
}
}
This is my bridge:
public class HybridJSBridge : Java.Lang.Object
{
Context context;
public HybridJSBridge(Context context)
{
this.context = context;
}
[JavascriptInterface]
[Export]
public void ShowToast(string msg)
{
Toast.MakeText(context, msg, ToastLength.Short).Show();
}
}
Right now nothing happens when the button is pressed. I hope someone can point me in the right direction?
Thanks in advance!
Do you want to achieve the result like following GIF?
I do not know what value of BaseUrl
in htmlSource.BaseUrl = DependencyService.Get<IBaseUrl>().Get(); , If I set the value of return "file:///android_asset";
[assembly: Dependency(typeof(BaseUrl_Android))]
namespace WebviewInvokeJS.Droid
{
class BaseUrl_Android : IBaseUrl
{
public string Get()
{
//throw new NotImplementedException();
return "file:///android_asset";
}
}
}
and add Control.SetWebViewClient(new WebViewClient()); in HybridWebViewRenderer it worked.
[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace WebviewInvokeJS.Droid
{
public class HybridWebViewRenderer : WebViewRenderer
{
Context _context;
public HybridWebViewRenderer(Context context) : base(context)
{
_context = context;
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (Control != null)
{
Control.Settings.JavaScriptEnabled = true;
Control.AddJavascriptInterface(new HybridJSBridge(Android.App.Application.Context), "CSharp");
Control.SetWebViewClient(new WebViewClient());
}
base.OnElementPropertyChanged(sender, e);
}
}
}
You also could download my demo to make a test.
https://github.com/851265601/XFormsWebviewInvokeJS
Related
I am developing a cross platform app that has a HybridWebView and displays a local html file. I have a wwwroot folder that contains the html file, css files, js files and all other resources. I have built the complete folder as BundleResource. I also start a local web server with EmbedIO. When I launch the app on iPads(iOS 15>), it does not execute the JavaScript files. On iPhones(iOS 15>) the app works fine. Also on iPads with iOS 12 the app works. Also, the app works on Safari no matter what device is used.
I have already added in info.plist NSAppTransportSecurity with NSAllowsArbitraryLoads = true.Also, I have developed a Swift app with a WebView and tried to use the local web server of the Xamarin.iOS app to present the app there. But again JavaScript is not executed (I also set the preferences regarding JavaScript).
My problem:
I don't understand why the application works on Safari, iPhones and old iPads, but not on new iPads. I suspect that you have to enable JavaScript, but can't find a corresponding solution.
To mention:
I load only one js file in the index.html. This js file in turn loads other js files (This works on all devices except the new iPads, as mentioned above).
Below I have added the HybridWebView.cs, the HybridWebViewRenderer.cs, the MainPage.xaml.cs and MainPage.xaml.
MainPage.xaml:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:extensions="clr-namespace:Viewer.Extensions;assembly=Viewer"
x:Class="Viewer.MainPage">
<StackLayout>
<extensions:HybridWebView x:Name="HybridWebView" Uri="{Binding WebViewSource}" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" />
</StackLayout>
</ContentPage>
MainPage.xaml.cs:
namespace Viewer
{
[DesignTimeVisible(false)]
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainPage : ContentPage
{
private readonly LocalWebServer _server = new LocalWebServer();
private string _webViewSource;
public string WebViewSource
{
get => _webViewSource;
set
{
_webViewSource = value;
OnPropertyChanged(nameof(WebViewSource));
}
}
public MainPage()
{
InitializeComponent();
BindingContext = this;
NavigationPage.SetHasNavigationBar(this, false);
HybridWebView.RegisterAction(data =>
{
DisplayAlert("Alert", "Hello " + data, "OK");
});
HybridWebView.RegisterQRAction(() =>
{
try
{
ZXingScannerPage scanPage = new ZXingScannerPage();
scanPage.OnScanResult += (result) =>
{
scanPage.IsScanning = false;
Device.BeginInvokeOnMainThread(async () =>
{
await Navigation.PopAsync();
var barcode = result.Text.ParseBarcode();
switch (barcode.BarcodeType)
{
case BarcodeType.Hotspot:
{
await HybridWebView.EvaluateJavaScriptAsync(
$"javascript:doLoadHS('{barcode.Datamodule}.html', '{barcode.Hotspot.figureId}', '{barcode.Hotspot.hotspotId}');");
break;
}
case BarcodeType.Datamodule:
default:
{
await HybridWebView.EvaluateJavaScriptAsync(
$"javascript:doLoad('{barcode.Datamodule}.html');");
break;
}
}
});
};
Device.BeginInvokeOnMainThread(() =>
{
Navigation.PushAsync(scanPage);
});
}
catch (Exception ex)
{
DisplayAlert("QR", $"Error while reading qr code: {ex.Message}", "OK");
}
});
HybridWebView.RegisterProjectSelectionAction(() =>
{
_server.Dispose();
Navigation.PopToRootAsync();
});
var docpath = Helper.PathAddBackslash(Path.Combine(DependencyService.Get<IApplicationConfigurationService>().DocumentationRootPath, Init.NAME_DIR_WWWROOT));
_server.StartWebServer(docpath, false, false);
WebViewSource = $"{LocalWebServer.Url}/index.html";
NavigationPage.SetHasBackButton(this, false);
}
protected override bool OnBackButtonPressed()
{
return true;
}
}
}
HybridWebView.cs:
using System;
using Xamarin.Forms;
namespace Viewer.Extensions
{
public class HybridWebView : WebView
{
Action<string> action;
Action qrAction;
Action projectSelectionAction;
public static readonly BindableProperty UriProperty = BindableProperty.Create(
propertyName: "Uri",
returnType: typeof(string),
declaringType: typeof(HybridWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
public void RegisterAction(Action<string> callback)
{
action = callback;
}
public void RegisterQRAction(Action callback)
{
qrAction = callback;
}
public void RegisterProjectSelectionAction(Action callback)
{
projectSelectionAction = callback;
}
public void Cleanup()
{
action = null;
qrAction = null;
projectSelectionAction = null;
}
public void InvokeAction(string data)
{
if (action == null || data == null)
{
return;
}
action.Invoke(data);
}
public void InvokeQRAction()
{
qrAction?.Invoke();
}
public void InvokeProjectSelectionAction()
{
projectSelectionAction?.Invoke();
}
}
}
HybridWebViewRenderer.cs:
[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace Viewer.iOS.Views
{
public class HybridWebViewRenderer : WkWebViewRenderer, IWKScriptMessageHandler
{
const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}" +
"function invokeCSharpQRAction(data){window.webkit.messageHandlers.invokeQRAction.postMessage(data);}";
WKUserContentController userController;
public HybridWebViewRenderer() : this(new WKWebViewConfiguration())
{
}
public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
{
userController = config.UserContentController;
var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
userController.AddUserScript(script);
userController.AddScriptMessageHandler(this, "invokeAction");
userController.AddScriptMessageHandler(this, "invokeQRAction");
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
userController.RemoveAllUserScripts();
userController.RemoveScriptMessageHandler("invokeAction");
userController.RemoveScriptMessageHandler("invokeQRAction");
HybridWebView hybridWebView = e.OldElement as HybridWebView;
hybridWebView.Cleanup();
}
if (e.NewElement != null)
{
LoadRequest(new NSUrlRequest(new NSUrl(((HybridWebView)Element).Uri)));
}
}
public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
{
var eventArgs = message.Body.ToString().ParseEventArgs();
switch (eventArgs.name)
{
case "invokeAction":
((HybridWebView)Element).InvokeAction(eventArgs.payload);
break;
case "invokeQRAction":
((HybridWebView)Element).InvokeQRAction();
break;
}
}
}
}
The problem here is probably with your Javascript.
Mobile and older devices often suppress the console errors of Javascript and instead of an error page or an exception, further Javascript is not executed and it seems as if it is not executed at all.
Especially with complex Javascript with many usings and references it is enough if there is a problem with one reference, so you don't see anything.
Here it can help to simplify the Javascript and to rebuild it bit by bit to localize the error.
I am loading a webpage in WebView. There is a link in the webpage, which on desktop will download the file, but in the app the link should display a Toast saying the link is disabled for the app.
I am not sure how to get the value from href of the anchor tag, when the link is clicked.
<a class="btn btn-primary" download="http://xx.xxx.com/wp-content/uploads/2015/11/abc-27-15.mp3" href="http://xx.xxx.com/wp-content/uploads/2015/11/abc-27-15.mp3">
<i class="fa fa-download"></i> Download Audio</a>
Can someone share an idea or any sample code on how to do this.
EDIT:1
Here is what I am doing currently:
private static final String URL = "http://xx.xxx.com/wp-content/uploads/";
webView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
WebView.HitTestResult hr = ((WebView) v).getHitTestResult();
String extra = hr.getExtra();
if (extra != null && extra.startsWith(URL) && extra.endsWith(".mp3")) {
Log.d("WebviewActivity", "Extra: " + extra);
Log.d("WebviewActivity", "Contains URL");
return true;
}
}
return false;
}
});
The problem with this approach is:
When i click on the link, i get the url in extra. It works fine till here. But, from next time, no matter where i click on the webview, the same extra is being returned. So even if i click on an image after i click on the url, i get the same url in the extra. Not sure if i doing anything wrong. Or is this the correct approach.
Please let me know if you need any details.
EDIT:2
private Handler mHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
// Get link-URL.
String url = (String) msg.getData().get("url");
// Do something with it.
if (url != null) {
Log.d(TAG, "URL: "+url);
}
}
};
webView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
WebView.HitTestResult hr = ((WebView) v).getHitTestResult();
if (hr.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) {
Message msg = mHandler.obtainMessage();
webView.requestFocusNodeHref(msg);
}
}
return false;
}
});
webView.loadUrl(mUrl);
}
Now, i get the URL that is clicked in the last action_down event. How to get the current URL?
EDIT 3 (Attempt with webviewclient:
private class MyWebViewClient extends WebViewClient {
private static final String URL = "xx.xxx.com/wp-content/uploads/";
#Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
if (!isFinishing())
mProgressDialog.show();
}
#Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mProgressDialog.dismiss();
}
#Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
Toast.makeText(WebviewActivity.this,
"Please check your internet " + "connection and try again",
Toast.LENGTH_SHORT).show();
}
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.d("xxx", "Url: " + url);
if(url.contains(URL)) {
Log.d("xxx", "Url Contains: " + URL);
return true;
}
return false;
}
}
mMyWebViewClient = new MyWebViewClient();
webView.setWebViewClient(mMyWebViewClient);
Output in logcat when the link is clicked:
03-01 15:38:19.402 19626-19626/com.xx.xxx D/cr_Ime: [ImeAdapter.java:553] focusedNodeChanged: isEditable [false]
03-01 15:38:19.428 19626-19626/com.xx.xxx D/cr_Ime: [ImeAdapter.java:253] updateKeyboardVisibility: type [0->0], flags [0], show [true],
03-01 15:38:19.428 19626-19626/com.xx.xxx D/cr_Ime: [ImeAdapter.java:326] hideKeyboard
03-01 15:38:19.429 19626-19626/com.xx.xxx D/cr_Ime: [InputMethodManagerWrapper.java:56] isActive: true
03-01 15:38:19.429 19626-19626/com.xx.xxx D/cr_Ime: [InputMethodManagerWrapper.java:65] hideSoftInputFromWindow
Because you are using a WebView and the link is not Java script this is very easy to achieve with a WebViewClient which you can use to monitor your WebView
myWebView.setWebViewClient( new WebViewClient() {
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// check something unique to the urls you want to block
if (url.contains("xx.xxx.com")) {
Toast.make... //trigger the toast
return true; //with return true, the webview wont try rendering the url
}
return false; //let other links work normally
}
} );
It's possible that because your URL ends in .mp3 the file is being treated as a resource. You should also override the shouldInterceptRequest method of the WebViewClient to check this.
#Override
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
Log.d("XXX", "Url from API21 shouldInterceptRequest : " + url);
if (url.contains(URL)) {
return new WebResourceResponse("text/html", "UTF-8", "<html><body>No downloading from app</body></html>");
} else {
return null;
}
}
public WebResourceResponse shouldInterceptRequest (WebView view, String url) {
Log.d("XXX", "Url from shouldInterceptRequest : " + url);
if (url.contains(URL)) {
return new WebResourceResponse("text/html", "UTF-8", "<html><body>No downloading from app</body></html>");
} else {
return null;
}
}
Most of the work can be done at the web page side itself. You have to write java script to identify which device is accessing the page (mobile, desktop etc) if its mobile then use java script binding technique to call the native android code to show Toast message.
http://developer.android.com/guide/webapps/webview.html
WebView webView = (WebView) findViewById(R.id.webview);
webView.addJavascriptInterface(new WebAppInterface(this), "Android");
WebAppInterface.java
public class WebAppInterface {
Context mContext;
/** Instantiate the interface and set the context */
WebAppInterface(Context c) {
mContext = c;
}
/** Show a toast from the web page */
#JavascriptInterface
public void showToast(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}
}
YourHTML Page (this sample got a button click)
<input type="button" value="Say hello" onClick="showAndroidToast('Hello
Android!')" />
<script type="text/javascript">
function showAndroidToast(toast) {
Android.showToast(toast);
}
</script>
Below code, I am using to load webview in fragment and I want to call JavaScript function from webview but below code is not working.
public class MainActivity extends ActionBarActivity
{
public void loadUrlInWebView(String url,String positon)
{
WebViewFragment fragment = new WebViewFragment();
Bundle data = new Bundle();
data.putString("url",url);
fragment.setArguments(data);
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.frame_container, fragment).commit();
}
var url ="javascript:mobileApp.openLoginMenu()";
loadUrlInWebView(url,null);
}
public class WebViewFragment extends Fragment{
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
url = getArguments().getString("url");
View rootView = inflater.inflate(R.layout.activity_main, container, false);
webView = (WebView) rootView.findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JsInterface(getActivity()),"AndroidJSObject");
webView.loadUrl(url);
webView.setWebViewClient(new WebViewClient()
{
public boolean shouldOverrideUrlLoading(WebView view, String url)
{
view.loadUrl(url);
return true;
}
});
return rootView;
}
}
To call a javascript into android you can call addJavascriptInterface().
For more explanation please follow this link Building Web Apps in WebView.
And the thing to keep in mind is,
When you want to call an android method from javascript method, the method name should be called on the string name you have provided while calling addJavascriptInterface() precisely in your case something like this in your javascript
AndroidJSObject.yourmethod
Inside javascript file.
if android:targetSdkVersion >=17
add #SuppressLint("JavascriptInterface") in onCreated method
and
class InJavaScript {
#JavascriptInterface
public void runOnAndroidJavaScript(String status) {
collectionStatus.setCollectionStatus(status);
}
}
watch out #JavasscriptInterface.
I'm trying to use javascript interface along with google maps javascript library.
I searched and i found a way almost all similar to this JavascriptInterface .
This is my code :
<script src="https://maps.googleapis.com/maps/api/js?v=3.exp"></script>
<script type="text/javascript">
function testAPI() {
JSInterface.test();
}
</script>
<script>
// gooogle maps code
//
</script>
</head>
<body>
<div id="map-canvas"></div>
<div><input type="button" value="Make Toast" onClick="testAPI()" /></div>
This is my interface :
public class JavaScriptInterface {
private Activity activity;
public JavaScriptInterface(Activity activiy) {
this.activity = activiy;
}
public void test(){
Toast.makeText(activity.getApplicationContext(),
"Javascript interface test.", 0).show();
}
}
This is where i create all :
myBrowser = (WebView) findViewById(R.id.mybrowser);
myBrowser.setWebChromeClient(new WebChromeClient() {
public void onGeolocationPermissionsShowPrompt(String origin,
GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);
}
});
myBrowser.getSettings().setJavaScriptEnabled(true);
String javascrips = null;
// myBrowser.loadUrl("file:///android_asset/geolocation.html");
try {
AssetManager am = getAssets();
InputStream input = am.open("geolocation.html");
int size;
size = input.available();
byte[] buffer = new byte[size];
input.read(buffer);
input.close();
javascrips = new String(buffer);
} catch (IOException e) {
e.printStackTrace();
}
jsInterface = new JavaScriptInterface(this);
myBrowser.addJavascriptInterface(jsInterface, "JSInterface");
myBrowser.loadDataWithBaseURL("file:///android_res/raw/", javascrips,
"text/html", "UTF-8", null);
When clicking on the button (the javascript input with the testAPI() function ) , nothing happens , The Toast doesn't not show up why ?
Try adding #JavascriptInterface to your test() method
http://developer.android.com/reference/android/webkit/JavascriptInterface.html
Need the annotation starting with JellyBean
Dear all i am just passing and returrn some value from javascript and android. I could able to pass value javascript to android. My problem is i could not able to return the value again. This is my snippet. can any body help me out
HTML and Script
<html>
<head>
<script src="phonegap-1.3.0.js" type="text/javascript"></script>
<script type="text/javascript">
function invoke(param1,param2)
{
alert('Hai');
//invoking the JavascriptBridge registered under the 'jb' namespace
var result = jb.callMe(param1,param2);
//doing something with the return value, it should be concatenation
//of the two input parameters
alert(result);
}
</script>
</head>
<body>
<form id = "returning">
<h2>Demonstrating Android Javascript-To-Java Bridge</h2>
<input type="button" value="Invoke Bridge" onclick="invoke('Hello','World');"/>
</form>
</body>
Android:
public class ReturnAndroidValActivity extends Activity
{
private WebView webView;
public ReturnAndroidValActivity()
{
}
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
}
#Override
protected void onStart()
{
super.onStart();
}
#Override
protected void onResume()
{
try
{
super.onResume();
//render the main screen
// this.setContentView(ViewHelper.findLayoutId(this, "main"));
//Find the WebView control
//this.webView = (WebView)ViewHelper.findViewById(this, "webview");
setContentView(R.layout.main);
this.webView = (WebView)findViewById(R.id.mybrowser);
//Enable Javascript...This is needed so that Javascript is allowed to execute
//inside the WebView
WebSettings webSettings = this.webView.getSettings();
webSettings.setJavaScriptEnabled(true);
//Register the 'Javascript Bridge' class under the 'jb' namespace
//this class can be invoked from the HTML/Javascript side
this.webView.addJavascriptInterface(new JavascriptBridge(), "jb");
//Register the WebChromeClient to assist with alerts/debugging
this.webView.setWebChromeClient(new MyWebChromeClient());
//Load assets/html/index.html resource into the WebView control
this.webView.loadUrl("file:///android_asset/www/index.html");
}
catch(Exception e)
{
e.printStackTrace(System.out);
}
}
final class JavascriptBridge
{
public String callMe(String param1, String param2)
{
//Generate the returnValue from the bridge
String toastValue = param1 + "," + param2;
//Setup the Toast
Toast toast = Toast.makeText(ReturnAndroidValActivity.this, toastValue, Toast.LENGTH_LONG);
//Show the Toast
toast.show();
return toastValue;
}
}
/**
* Provides a hook for calling "alert" from javascript. Useful for
* debugging your javascript.
*/
final class MyWebChromeClient extends WebChromeClient
{
#Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result)
{
Log.d("JavascriptBridge", message);
resu lt.confirm();
return true;
}
}
}
Define one more function in the javascript:
<script>
function my_callback_function(param){
alert("Called with value: " + param);
}
</script>
Then you call this function through the WebView in the native code like that:
webView.loadUrl("javascript:my_callback_function('TheValue')");