UPDATE: it's a confirmed bug. Please upvote it here because it doesn't really receive a lot of attention from MS.
I need to override the shouldInterceptRequest method of WebViewClient to load in-app HTML content following that guide.
Here's the repo with the reproducible code: GitHub. I took a sample code from MS Q&A as well:
// ...
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<Microsoft.Maui.Controls.WebView, ProblemHandler2>();
});
// ...
internal class ProblemHandler2 : WebViewHandler
{
protected override Android.Webkit.WebView CreatePlatformView()
{
var wv = new Android.Webkit.WebView(Android.App.Application.Context);
wv.SetWebViewClient(new CustomWebClient());
return wv;
}
}
In the repo, I included 2 custom handlers:
ProblemHandler2 is the exact snippet by the MSFT. I realized a problem: Setting MAUI WebView's Source property no longer navigates the real Android WebView:
WebViewHandler.Mapper.AppendToMapping("MyHandler", (handler, view) =>
{
#if ANDROID
var xWv = handler.PlatformView;
// For ProblemHandler2, this is needed to actually navigate:
xWv.LoadUrl("https://www.google.com/");
#endif
});
this.wv.Source = "https://www.google.com/";
ProblemHandler1 uses the default result and adds a custom handler. This fixes the navigation problem, but, both problem have the same issue:
ShouldInterceptRequest is never called. It is never called on anything even when I manually click a link to navigate. What am I missing? I am sure the CustomWebClient is correctly created and set.
I noticed none of the other callbacks works as well, for example:
public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
{
Debugger.Break();
Debug.WriteLine(url);
base.OnPageStarted(view, url, favicon);
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
Debugger.Break();
Debug.WriteLine(url);
base.OnPageFinished(view, url);
}
I also tried using WebViewHandler.Mapping but it also does not work:
WebViewHandler.Mapper.AppendToMapping("MyHandler", (handler, _) =>
{
#if ANDROID
handler.PlatformView.SetWebViewClient(new CustomWebClient());
#endif
});
I could be wrong but, I think this might have to do with your overridden version of the CreatePlatform method,
Can you try what the default WebViewHandler is doing:
protected override WebView CreatePlatformView()
{
var platformView = new MauiWebView(this, Context!)
{
LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
};
platformView.Settings.JavaScriptEnabled = true;
platformView.Settings.DomStorageEnabled = true;
platformView.Settings.SetSupportMultipleWindows(true);
return platformView;
}
Check this URL for the default handlers CreatePlatform setup :
https://github.com/dotnet/maui/blob/c6250a20d73e1992b4a02e6f3c26a1e6cbcbe988/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs
Also don't use Application Context in Handlers, Handlers have their own Context property you can use.
Yes, it is the case as you said.
And I have created a new issue for this problem, you can follow it up here: https://github.com/dotnet/maui/issues/11004.
Thanks for your support and feedback for maui.
Best Regards.
Related
I want to use NativeScript's WebView to preprocess some of my data (it uses XHTML so I must use WebView and it's evaluateJavascript method).
But for the preprocess I don't want to render the whole WebView just for a blink of an eye time, so I created a service which creates a new WebView() instance. Now I can set the src attribute this way and all others, like height and with, but if I want to call the evaluateJavascript function I can't. Code below.
test() {
//create new webview instance
this.wv = new WebView();
//all of this works
this.wv.set('height', 100);
this.wv.setProperty('width', 200);
this.wv.setProperty('src', 'some url here');
//this does not work
this.wv.android.evaluateJavascript(
'function test(){ return 1;} test();',
new android.webkit.ValueCallback({
onReceiveValue: function (res: any) {
console.log(res);
}
})
);
}
The set and setProperty methods are fine, but the evaluateJavascript says it can't be called on undefined.
console.log(this.wv.android); //undefined
Also the WebView loaded event does not run, if have something like this in the test function above.
this.wv.on('loadFinished', (data) => {
console.log('loadFinished');
console.dir(data);
});
On the other hand, if I create a component, put a WebView element in it, than listen for the loadFinished event, than i can run the JS seen above, like this:
webViewLoaded(webargs) {
// console.log('WEBVIEW LOADED');
this.webView = webargs.object;
this.webVie.android.evaluateJavascript(
'function test(){ return 1;} test();',
new android.webkit.ValueCallback({
onReceiveValue: function (res: any) {
console.log(res);
}
})
);
}
//It run properly.
}
So my questions are:
Am I right and the loadFinished event does not run on the creation of new WebView() instance? If it does run, how can I add a listener to it?
What's the difference between the instance of new WebView() and the object returned in the args param above? And why this.wv.android is undefined in the test function above?
Can I run evaluateJavascript on a WebView instance that does not have any HTML counterpart and is not even rendered out? If yes, how?
Am I obligated to render the WebView to use it properly?
I am using the following code for injecting Java Script in to my Android Web view
WebView
webView = FindViewById<WebView> (Resource.Id.learningWebView);
if (null != webView) {
webView.Settings.JavaScriptEnabled = true;
webView.Settings.SetSupportZoom (true);
webView.SetWebViewClient (new CustomWebViewClient ());
}
WebView Client implementation
public class CustomWebViewClient : WebViewClient
{
public override bool ShouldOverrideUrlLoading (WebView view, string url)
{
view.LoadUrl (url);
return true;
}
public override void OnPageStarted (WebView view, string url, Android.Graphics.Bitmap favicon)
{
}
public override void OnPageFinished (WebView view, string url)
{
base.OnPageFinished (view, url);
HideLearningDivs (view);
}
void HideLearningDivs (WebView view)
{
try {
view.EvaluateJavascript ("document.getElementById(\"suiteBar\").parentNode.style.display='none'", new JavaScriptResult ());
} catch (Exception ex) {
Console.WriteLine (ex.Message);
}
}
IValueCallback Implementation
public class JavaScriptResult : IValueCallback
{
public IntPtr Handle {
get;
set;
}
public void Dispose ()
{
}
public void OnReceiveValue (Java.Lang.Object result)
{
}
}
But during the time of executing the application I am getting the following error.
java.lang.NoSuchMethodError: no method with name='evaluateJavascript' signature='(Ljava/lang/String;Landroid/webkit/ValueCallback;)V' in class Landroid/webkit/WebView;
Can anyone please help me to find what is wrong with my implementation.
I will link to where I found the answer below, but basically you need to do a check for Android KitKat (4.4), since that function was not introduced until then. If the device is running lower than 4.4, then you may need to do something different to get the value back if you actually need to do something with it. Such as using a Hybrid WebView of some kind (check out Xamarin Forms Labs version of it perhaps) and/or using the AddJavaScriptInterface()
Here is the code:
if(Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat) {
webView.EvaluateJavascript("javascript:GoBack();", null);
} else {
webView.LoadUrl("javascript:GoBack();");
}
https://forums.xamarin.com/discussion/24894/webview-evaluatejavascript-issues
*Edit: Since writing this, I found an excellent post Adam Pedley (who I apparently have been linking to a lot lately) which covers doing this for Xamarin Forms but also mentions a change in Android 4.2 to the JS engine. Running the JavaScript might work the first time but it also sets the document object to the script result, so you may need to assign the result of document.getElementById() to a variable in order to work around this: var x = document.getElementById().
JavaScriptResult class must be inherited from Java.Lang.Object, like this:
public class JavaScriptResult : Java.Lang.Object, IValueCallback
{
public void OnReceiveValue(Object value)
{
// ...
}
}
I'm writing a simple game. I included HTML pages + scrits into the apk file (I put them into assets folder and then loaded into a webview)
browser.loadUrl("file:///android_asset/home.html");
The prototype was only for one player and it was working fine till I needed to add authorization (to have the ability to play with a human:). I created a page for that and did a request:
$.mobile.showPageLoadingMsg();
$.ajax({
url: 'https://www.myhost.com/login',
data: {mail:'admin#mail.com', password: '123'},
datatype: 'html',
type: 'POST',
success:function(html){
$.mobile.hidePageLoadingMsg();
$("#message").html('SUCCESS');
},
error: function(jqXHR, textStatus, errorThrown) {
$("#message").html('FAILED');
$.mobile.hidePageLoadingMsg();
}
})
This call works fine. But... then I needed to add account info to all the game pages so the server can know who is doing what. So...I decided t create a bridge for that:
browser.addJavascriptInterface(bridge, "Bridge");
On the Java side I created a function that stores success login info in the Application class (something like a session).
#JavascriptInterface
public void storeAuthData(String callback, String user, String pass) {
WebGameApplication.setUser(user);
WebGameApplication.setPassword(pass);
webview.loadUrl("javascript:"+callback);
}
And it also works. But how can I add this params if I have POST requests? I googled a lot.
For GET requests it's possible to do it here:
#Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
Log.i(TAG, "shouldInterceptRequest: " + url);
return super.shouldInterceptRequest(view, url);
}
But for POST I'm stuck. Maybe the solution I picked is incorrect? Of course I can extend the bridge by adding one extra method to retrive account info and then include this into each call whether it GET or POST.... but Really? There's no way of doing that?
Thanks in advance.
I'm afraid that POST requests are not passed through shouldInterceptRequest, only GET.
Could you set a cookie with the auth data rather than (or in addition to) using a JavaScript Bridge?
FYI, you should update your storeAuthData function such that it posts a task to the WebView, something like this:
#JavascriptInterface
public void storeAuthData(String callback, String user, String pass) {
...
webview.post(new Runnable() {
#Override
public void run() {
webview.loadUrl("javascript:"+callback);
}
});
}
The reason for this is important - WebView methods must be run on your applications UI thread, and JavaScript bridge callbacks are executed on a native background thread.
I'm working on making a browser as a hybrid app using worklight framework for Android. I implemented my address bar as an input element which received the user input and pass the arguments to the webview to load the page.
However, I cannot figure out how to do the reverse: whenever the user click on a link in webview, I want the address bar to change to the new location.
Are you implementing a native page that is opened? If so, take a look at ChildBrowser, that basically does the same thing. It has a TextView being used as an address bar. You may decide to use it, or get the bits and pieces you want out of it. Regardless, I would image what you want to do something like this. By overriding the onLoadResource in the WebViewClient, you should be able to grab the url and change your TextBox.
In response to the comment below: inside your environment's main js file in the wlEnvInit() function:
function wlEnvInit(){
wlCommonInit();
// Environment initialization code goes here
document.onclick=manageLinks;
}
Then in this function get the url and set the text of your input element:
function manageLinks(event) {
var link = event.target;
//go up the family tree until we find the A tag
while (link && link.tagName != 'A') {
link = link.parentNode;
}
if (link) {
var url = link.href;
console.log("url = " + url);
//You can decide if you want to separate external or
//internal links, depending on your application
var linkIsExternal = ((url.indexOf('http://') == 0) || (url.indexOf('https://') == 0));
if (linkIsExternal) {
myInput.setText(url);
return false;
}
}
return true;
}
Inside of your WebView, inside the plugin, intercept the URL like this:
webview.setWebViewClient(new WebViewClient() {
#Override
public void onPageFinished(WebView view, String url) {
//use this area to set your input. Depending on how you
//implemented your plugin, you may need to return this value
//back to your main activity
Toast.makeText(cordova.getActivity(), "Loading: " + url, Toast.LENGTH_LONG).show();
}
});
Have you try to get the url from the href of and assign to the input variable and do the get/post? I know that it is possible in SDK i figure it dont will be harder in a framework. You can store the hiperlinks in a array with a parser or something similar.
example pseudocode:
When_hiperlink_clicked: //could be like a listener (search about it)
url = hiperlink.getURL("myHiperlink");
myinput.setText(url);
execute_input_bar_action();
Is difficult to figure out without code or something more, sorry.
I am trying to perform an action on an Android WebView after my webpage finishes loading in it. I setup my WebView to have a WebViewClient to make use of the onPageFinished event callback. However, after some testing, it does't seem to wait until all the JS on the page is done loading before my onPageFinished code fires.
The Google documentation says this:
public void onPageFinished (WebView view, String url)
Added in API level 1
Notify the host application that a page has finished loading. This method is called only for main frame. When onPageFinished() is called, the rendering picture may not be updated yet. To get the notification for the new Picture, use onNewPicture(WebView, Picture).
Parameters
view The WebView that is initiating the callback.
url The url of the page.
1) Does onPageFinished only wait for the DOM to load?
2) Is there a way to detect when any JS on the page finishes? If so, what should I use?
I don't see anything in WebViewClient that would be for that purpose. I don't want to add a delay since my users can be on EDGE or on LTE.
You need to implement the callbacks from WebChromeClient. The onPageFinished() is an API that is provided by WebViewClient. There is yet another interface named WebChromeClient that provides the progress information you are seeking:
http://developer.android.com/reference/android/webkit/WebChromeClient.html#onProgressChanged(android.webkit.WebView, int)
Open the link above and look for onProgressChanged(WebView view, int newProgress) - the 'newProgress' variable gives you the percentage of page load that was completed. When it reaches 100 you have a valid page. onPageFinished() cannot be reliably used for this (due to server side redirections etc)
I don't know what you mean by "when JS on the page is finished". Maybe you can clarify what you meant?
From:
https://chromium.googlesource.com/chromium/src.git/+/master/android_webview/java/src/org/chromium/android_webview/AwWebContentsObserver.java
#Override
public void didFinishNavigation(final String url, boolean isInMainFrame, boolean isErrorPage,
boolean hasCommitted, boolean isSameDocument, boolean isFragmentNavigation,
Integer pageTransition, int errorCode, String errorDescription, int httpStatusCode) {
...
if (client != null && isFragmentNavigation) {
client.getCallbackHelper().postOnPageFinished(url);
}
}
#Override
public void didFailLoad(
boolean isMainFrame, int errorCode, String description, String failingUrl) {
AwContentsClient client = mAwContentsClient.get();
if (client == null) return;
String unreachableWebDataUrl = AwContentsStatics.getUnreachableWebDataUrl();
boolean isErrorUrl =
unreachableWebDataUrl != null && unreachableWebDataUrl.equals(failingUrl);
if (isMainFrame && !isErrorUrl && errorCode == NetError.ERR_ABORTED) {
// Need to call onPageFinished for backwards compatibility with the classic webview.
// See also AwContents.IoThreadClientImpl.onReceivedError.
client.getCallbackHelper().postOnPageFinished(failingUrl);
}
}
#Override
public void didStopLoading(String validatedUrl) {
if (validatedUrl.length() == 0) validatedUrl = ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL;
AwContentsClient client = getClientIfNeedToFireCallback(validatedUrl);
if (client != null && validatedUrl.equals(mLastDidFinishLoadUrl)) {
client.getCallbackHelper().postOnPageFinished(validatedUrl);
mLastDidFinishLoadUrl = null;
}
}
We can easily see that onPageFinished is not pretty much what you're expecting.
To answer your first question: I have found that onProgressChanged will not reach 100, and onPageFinished will not be called until all of the assets (css/js/images) have finished loading for that page.
I can not, however, find any official documentation that states that.