Xamarin Android WebView don't fire navigating if "target=_blank" - android

I'm using a webview in xamarin, i followed many tutorials to handle navigation, and all works fine.
My issue is : when an anchor tag has a target="_blank" the event Navigating is never fired.
I see arround someone give a javascript solution which remove target=_blank and attach it at the end of href link.
Is really that the right way to do that? Look wired..
Thank you
This is initialization in xamarin.android renderer
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
global::Android.Webkit.WebView.SetWebContentsDebuggingEnabled(true);
if (e.OldElement != null)
{
Control.RemoveJavascriptInterface("jsBridge");
((HybridWebView)Element).Cleanup();
}
if (e.NewElement != null)
{
Control.Settings.JavaScriptEnabled = true;
Control.Settings.DomStorageEnabled = true;
Control.Settings.JavaScriptCanOpenWindowsAutomatically = true;
Control.Settings.SetSupportMultipleWindows(true);
Control.Settings.AllowFileAccessFromFileURLs = true;
Control.Settings.AllowUniversalAccessFromFileURLs = true;
Control.Settings.UserAgentString = Control.Settings.UserAgentString + " crmvw";
Android.Webkit.WebChromeClient xCC = new CustChromeWebViewClient(_context);
Control.SetWebChromeClient(xCC);
Control.SetWebViewClient(new CrmWebViewClient(this, $"javascript: {JavascriptFunction}"));
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
Control.LoadUrl(((HybridWebView)Element).Uri);
}
}
And this is my navigating event, never fired when anchor has target=_blank
private void webv_Navigating(object sender, WebNavigatingEventArgs e)
{
if (IsFirstLoad) {
IsBusy = true;
IsFirstLoad = false;
}
if (e.Url.ToLower().StartsWith("tel:") || e.Url.ToString().StartsWith("wtai:") || e.Url.ToLower().StartsWith("sms:") || e.Url.ToLower().StartsWith("mailto:"))
{
e.Cancel = true;
}
}
here my override function for URL in my custom WEBView
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, Android.Webkit.IWebResourceRequest request)
{
Android.Net.Uri url = request.Url;
if (url.ToString().StartsWith("tel:") || url.ToString().StartsWith("wtai:"))
{
Xamarin.Essentials.PhoneDialer.Open(UtilityXam.Contact.GetPhoneFromHTML(url.ToString()));
return true;
}else if (url.ToString().StartsWith("mailto:"))
{
UtilityXam.Contact xE = new UtilityXam.Contact();
string xEmail = UtilityXam.Contact.GetEmailFromHTML( url.ToString());
var xTask = xE.SendEmail("","",new System.Collections.Generic.List<string>(){ xEmail });
return true;
}
else if (url.ToString().StartsWith("sms:"))
{
UtilityXam.Contact xE = new UtilityXam.Contact();
string xPh = UtilityXam.Contact.GetPhoneFromHTML(url.ToString());
var xTask = xE.SendSMS("", "", new System.Collections.Generic.List<string>() { xPh });
}
else
{
view.LoadUrl(url.ToString());
}
view.SetDownloadListener(new CrmDownloadListener(_context));
return true;
}

After the great help of Jack Hua i'm able to solve the problem.
In OnElementChanged of Hybrid renderer i set support for multiple windows.
Control.Settings.SetSupportMultipleWindows(true);
and next i had to menage onCreateWindow event in the custom chrome webview.
Here the code converted in c# from the link suggested by Jack.
public override bool OnCreateWindow(Android.Webkit.WebView view, bool isDialog, bool isUserGesture, Android.OS.Message resultMsg)
{
Android.Webkit.WebView newWebView = new Android.Webkit.WebView(_context);
view.AddView(newWebView);
Android.Webkit.WebView.WebViewTransport transport = (Android.Webkit.WebView.WebViewTransport) resultMsg.Obj;
transport.WebView = newWebView;
resultMsg.SendToTarget();
return true;
}

This is an introduced bug in Xamarin Forms since v4.8.0.1364 (According to the bug report at least)
You can work around it for now by removing the target="_blank" from the url or by setting a property
webView.Settings.SetSupportMultipleWindows(true);
I have fixed it for our app by striping target="_blank" and target='_blank' in some replacement logic that already runs over the content
There are multiple open issues reporting it for Xamarin Forms github
[Bug] Cannot open URLs with WebView android when the target is _blank #12917
[Bug] Android WebView's Navigating and Navigated events not fired #12852

I tried a completely different approach, because the above answers didn't really help (my target _blank links would always open in a new chrome instance and not in the in-app browser).
First, you'll need to set SetSupportMultipleWindows to false. As soon as you do that, all the windows will open in the same webView:
Control.Settings.SetSupportMultipleWindows(false);
More information on how you set these settings: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/hybridwebview
Next, all I did was change the back-button behaviour, to make sure the back button doesn't close the app and instead navigates the webview pages (HybridWebView is my custom webview that I created in the first step).
HybridWebView _browser;
public MainPage()
{
_browser = new HybridWebView
{
Source = "https://brandschutzplaner-stahltragwerke.promat.ch"
};
Content = _browser;
}
protected override bool OnBackButtonPressed()
{
base.OnBackButtonPressed();
if (_browser.CanGoBack)
{
_browser.GoBack();
return true;
}
else
{
base.OnBackButtonPressed();
return true;
}
}

Related

Android Appcelerator Get URL from remote URL in webview and open it in devices default browser or a new webview

My question is exactly the same as this one but for Android and not iOS.
Get URL from remote URL in webview and open it in safari
Anyone have an idea. I am creating a cross-platform app and I have used the Clayton's answer to get it to work for iOS with some tweaks to open with a controller. But when trying different methods on Android and it is not working. This is as close as I have gotten (which is what Aaron provided on that same page) and it is not quite right as it opens the remote web page in a new browser window as well in the apps webview:
$.floorView.addEventListener('load', function(e) {
if (e.url.indexOf("http") !== -1) {
// stop the event
e.bubble = false;
// stop the url from loading
$.floorView.stopLoading();
// open
Ti.Platform.openURL(e.url);
}
});
Thanks!
I'd listen to the beforeload event, although I'm not 100% sure if you can actually prevent the Webview from still continuing the load as well.
Another way would be to intercept these links via JS you load or inject (evalJS()) in the webpage. Then fire a Ti.App event and respond to it in Titanium.
The Titanium.UI.Webview has a specific property for intercepting links: onlink.
This is not implemented as an event because it is a callback and needs to return a boolean to tell the Webview whether or not to load the URL of the link.
Oddly, setting the onlink callback right away makes the URL immediately load in Safari, so I did it this way:
$.webview.addEventListener('load', function(e) {
$.webview.onlink = function(e) {
Ti.Platform.openURL(e.url);
return false;
};
});
You can of course check the e.url string and decide whether to open it internally or externally.
I think I may have figured it out. Thanks to those whose ideas and suggestions lead to this code.
It appears to be working as I want on iOS and Android. Any suggestions or issues that you guys have I would appreciate the feedback. Thanks!
if ("iOS") {
$.webView.addEventListener('beforeload', function(e) {
if (e.navigationType == Titanium.UI.iOS.WEBVIEW_NAVIGATIONTYPE_LINK_CLICKED) {
// stop the event
e.bubble = false;
// stop the url from loading
$.webView.stopLoading();
//opens up the clicked URL for bill in new webView
var link = e.url;
var args = {url: link,};
// open link in my default webView for iOS
var newWebView=Alloy.createController('defaultWebView', args).getView();
newWebView.open();
}
});
}
else if ("Android") {
$.webView.addEventListener('beforeload', function(e) {
if (e.url.indexOf("http") !== -1) {
function Parser(text) {
var html = text;
var urlRegex = /((http|https):\/\/(\w+:{0,1}\w*#)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%#!\-\/]))?)/gi;
this.getHTML = function() {
return html;
};
} // end Parser
var parser = new Parser(e.url);
html = parser.getHTML();
if (html != "url of $.webView") {
// stop it from loding in current webView
$.webView.stopLoading();
// open link in browser
Ti.Platform.openURL(html);
}
}
});
}
else {
.....................
}

Issue with WebView.EvaluateJavaScript in Android Xamarin

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)
{
// ...
}
}

Getting "Link text" of the link displayed in WebView

I have Android WebView which displays some links as: Link1TextLink2Text Now I would like to retrieve Link1Text and Link2Text when I long press these links. I have contextMenu implemented in the code and I could successfully get the link urls (http://link1.html, http://link2.html) using HitTestResult getExtra() method but how ccan I get those link texts ?FYI, I require those link texts for implementing "Copy link text" option in the contextMenu.
To get the text of an achor link:
I. Hook a touchstart listener to every web pages in the onPageFinished() callback of WebViewClient via evaluateJavascript. like:
//Javascripts to evaluate in onPageFinished
const w=window;
w.addEventListener('touchstart',wrappedOnDownFunc);
function wrappedOnDownFunc(e){
if(e.touches.length==1){
w._touchtarget = e.touches[0].target;
}
console.log('hey touched something ' +w._touchtarget);
}
note we've saved the touch target.
II. Then implement OnLongClicklisenter for webview. use evaluateJavascript again when you long pressed on a link object:
#Override
public boolean onLongClick(View v) {
WebView.HitTestResult result = ((WebView)v).getHitTestResult();
if (null == result) return false;
int type = result.getType();
switch (type) {
case WebView.HitTestResult.SRC_ANCHOR_TYPE:
if(result.getExtra()!=null){
((WebView)v).evaluateJavascript("window._touchtarget?window._touchtarget.innerText:''", new ValueCallback<String>() {
#Override
public void onReceiveValue(String value) {
System.out.println("hey received link text : "+value);
}
});
}
return true;
}
return false;
}
What's more, we can even choose to select the text of the anchor element! Actually this is one of the options that samsung browser offers when you long-pressed an tag .
To achieve this, we still need that recorded touch target. Besides we need 2 new javascript methods:
function selectTouchtarget(){
var tt = w._touchtarget;
if(tt){
w._touchtarget_href = tt.getAttribute("href");
tt.removeAttribute("href");
var sel = w.getSelection();
var range = document.createRange();
range.selectNodeContents(tt);
sel.removeAllRanges();
sel.addRange(range);
}
}
function restoreTouchtarget(){
var tt = w._touchtarget;
if(tt){
tt.setAttribute("href", w._touchtarget_href);
}
}
Finnaly in the onLongClick listener, instead of just fetch the innerText, we programmatically set the selection, trigger the action menu bar, and restore the removed href attribute of our touch target.
case WebViewmy.HitTestResult.SRC_ANCHOR_TYPE:
if(result.getExtra()!=null){
WebViewmy mWebView = ((WebViewmy)v);
mWebView.evaluateJavascript("selectTouchtarget()", new ValueCallback<String>() {
#Override
public void onReceiveValue(String value) {
/* bring in action mode by a fake click on the programmatically selected text. */
MotionEvent te = MotionEvent.obtain(0,0,KeyEvent.ACTION_DOWN,mWebView.lastX,mWebView.lastY,0);
mWebView.dispatchTouchEvent(te);
te.setAction(KeyEvent.ACTION_UP);
mWebView.dispatchTouchEvent(te);
te.recycle();
//if it's not delayed for a while or the href attribute is not removed, then the above code would click into
// the anchor element instead of select it's text.
/* restore href attribute */
mWebView.postDelayed(() -> mWebView.evaluateJavascript("restoreTouchtarget()", null), 100);
}
});
}
return true;
In my case, I've extended the WebView as WebViewmy to record last touched positions, lastX and lastY, in the onTouchEvent method.
Unfortunately, a clear, official way to do this is not available. Although, there are two APIs (selectText and copySelection) which are pending API council approval, that may help to do this, but they are not available at the moment.

How to get link-URL in Android WebView with HitTestResult for a linked image (and not the image-URL) with Longclick

I try to catch webview longclicks to show a context menu. (see code below)
When longclicking an image, I always get the image-URL as extra (for a not linked image with IMAGE_TYPE and for a linked image with SRC_IMAGE_ANCHOR_TYPE).
But how can I get the Link-URL (and not the image-URL) for an image with a hyperlink?
Best,
Sebastian
mywebview.setOnLongClickListener(new OnLongClickListener() {
public boolean onLongClick(View v) {
final WebView webview = (WebView) v;
final WebView.HitTestResult result = webview.getHitTestResult();
if (result.getType() == SRC_ANCHOR_TYPE) {
return true;
}
if (result.getType() == SRC_IMAGE_ANCHOR_TYPE) {
return true;
}
if (result.getType() == IMAGE_TYPE) {
return true;
}
return false;
}
});
None of solutions above worked for me on Android 4.2.2. So I looked into source code of default android web browser. I extracted solution to this exact problem - get link-URL from image link.
Source:
https://github.com/android/platform_packages_apps_browser/blob/master/src/com/android/browser/Controller.java
Extracted solution:
LongClick listener:
...
mWebview.setOnLongClickListener(new OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
HitTestResult result = mWebview.getHitTestResult();
if (result.getType() == HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
Message msg = mHandler.obtainMessage();
mWebview.requestFocusNodeHref(msg);
}
}
});
...
Handler to get the URL:
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) ...
}
};
I know this is an old issue, but I recently came across this issue. Based on Perry_ml answer, I used the following Kotlin code to resolve it:
webView.setOnLongClickListener {
val result = webView.hitTestResult
if (result.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
val handler = Handler()
val message = handler.obtainMessage()
webView.requestFocusNodeHref(message)
val url = message.data.getString("url")
// Do something with url, return true as touch has been handled
true
} else {
false
}
}
I posted some information about it here.
I checked the source code of the WebView and it seems that the image uri is the only extra data you can get for SRC_IMAGE_ANCHOR_TYPE. But don't be mad here I have a quick and dirty workaround for you:
webview.setOnLongClickListener(new OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
final WebView webview = (WebView) v;
final HitTestResult result = webview.getHitTestResult();
if(result.getType()==HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
webview.setWebViewClient(new WebViewClient(){
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 2. and here we get the url (remember to remove the WebView client and return true so that the hyperlink will not be really triggered)
mUrl = url; // mUrl is a member variant of the activity
view.setWebViewClient(null);
return true;
}
});
// 1. the picture must be focused, so we simulate a DPAD enter event to trigger the hyperlink
KeyEvent event1 = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
webview.dispatchKeyEvent(event1);
KeyEvent event2 = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
webview.dispatchKeyEvent(event2);
// 3. now you can do something with the anchor url (and then clear the mUrl for future usage)
String url = mUrl;
if (url!=null) {
Toast.makeText(webview.getContext(), url, Toast.LENGTH_SHORT).show();
}
mUrl = null;
}
return false;
}
});
I tried the code on a low-end Android 2.1 device and a high-end Android 4.0 device, both work like a charm.
Regards
Ziteng Chen
Ziteng Chen solution works up to Android 4.0 (API Level 15) but for some reason the KeyEvent down & up doesn't work in API LEVEL 16+ (Android 4.1+ JELLY_BEAN). It doesn't fire the WebView's loadUrl. So I had to replace the dispatchKeyEvent with dispatchTouchEvent. Here's the code:
...
MotionEvent touchDown = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, touchX, touchY, 0);
webView.dispatchTouchEvent(touchDown);
touchDown.recycle();
MotionEvent touchUp = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, touchX, touchY, 0);
webView.dispatchTouchEvent(touchUp);
touchUp.recycle();
String url = mUrl;
...
You'd probably have to wait (use an AsyncTask) to get the mUrl in slower devices where it's null immediately after firing the dispatchTouchEvents
Hope it helps.
Instead of calling this function myWebView.requestFocusNodeHref(msg);, try calling this function myWebView.requestImageRef(msg);

Retrieve User-Agent programmatically

Is there a way to retrieve Browser's user-agent without having a WebView in activity?
I know it is possible to get it via WebView:
WebView view = (WebView) findViewById(R.id.someview);
String ua = view.getSettings().getUserAgentString() ;
But in my case I don't have/need a webview object and I don't want to create it just for retrieving user-agent string.
If you don't have one you can try taking it like this
String ua=new WebView(this).getSettings().getUserAgentString();
Edit-
The doc for getUserAgentString() says
Return the WebView's user-agent string.
So i don't think you can get it unless you declare one. Some one correct me if i am wrong
There is a much simpler way if you are on Android 2.1 or above. Granted this isn't the exact same User Agent string that a webview would return, but might serve you well enough for your purposes.
As an additional advantage to pulling from web view, you can use this from any thread (not just the UI thread).
There is a system property called http.agent, which can be used to retrieve the User-Agent string.
String userAgent = System.getProperty("http.agent");
See Programmatically get User-Agent String for more details.
I used to use solution proposed by DeRagan. But it turned out that creating a single WebView instance starts a thread "WebViewCoreThread" which stays on the background until application is terminated by the system. Maybe it doesn't consume too much resources but I don't like it anyway. So I use slightly different method now, which tries to avoid WebViewCoreThread creation:
// You may uncomment next line if using Android Annotations library, otherwise just be sure to run it in on the UI thread
// #UiThread
public static String getDefaultUserAgentString(Context context) {
if (Build.VERSION.SDK_INT >= 17) {
return NewApiWrapper.getDefaultUserAgent(context);
}
try {
Constructor<WebSettings> constructor = WebSettings.class.getDeclaredConstructor(Context.class, WebView.class);
constructor.setAccessible(true);
try {
WebSettings settings = constructor.newInstance(context, null);
return settings.getUserAgentString();
} finally {
constructor.setAccessible(false);
}
} catch (Exception e) {
return new WebView(context).getSettings().getUserAgentString();
}
}
#TargetApi(17)
static class NewApiWrapper {
static String getDefaultUserAgent(Context context) {
return WebSettings.getDefaultUserAgent(context);
}
}
It creates WebSettings instance directly using package-visible constructor and if that is not available for some reason (e.g. due to API changes in future Android versions) - silently falls back to "WebView-like" solution.
UPDATE
As pointed by #Skywalker5446, starting from Android 4.2/API 17, there is a public static method to get default user agent value. I've updated my code to use that method on the supported platforms.
Since Android 2.1 you should use System.getProperty("http.agent");
You also dont need to create a WebView first AND , thats the advantage,
you can use it inside a non-uithread.
greetings steve
This is an updated solution based on previous answers that works when you compile for KitKat. Now the WebSettings class is abstract and the WebSettingsClassic class has been removed.
#TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public static String getUserAgent(final Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return WebSettings.getDefaultUserAgent(context);
}
else {
try {
final Class<?> webSettingsClassicClass = Class.forName("android.webkit.WebSettingsClassic");
final Constructor<?> constructor = webSettingsClassicClass.getDeclaredConstructor(Context.class, Class.forName("android.webkit.WebViewClassic"));
constructor.setAccessible(true);
final Method method = webSettingsClassicClass.getMethod("getUserAgentString");
return (String) method.invoke(constructor.newInstance(context, null));
}
catch (final Exception e) {
return new WebView(context).getSettings()
.getUserAgentString();
}
}
}
Thanks to Idolon's answer my app could process this in the background.
But somehow on HTC Inspire 4G from AT&T that runs 2.3.3, it goes to the catch statement and it can be no longer run on the background thread.
My solution for this is the following:
public static String getUserAgent(Context context) {
try {
Constructor<WebSettings> constructor = WebSettings.class.getDeclaredConstructor(Context.class, WebView.class);
constructor.setAccessible(true);
try {
WebSettings settings = constructor.newInstance(context, null);
return settings.getUserAgentString();
} finally {
constructor.setAccessible(false);
}
} catch (Exception e) {
String ua;
if(Thread.currentThread().getName().equalsIgnoreCase("main")){
WebView m_webview = new WebView(context);
ua = m_webview.getSettings().getUserAgentString();
}else{
mContext = context;
((Activity) mContext).runOnUiThread(new Runnable() {
#Override
public void run() {
WebView webview = new WebView(mContext);
mUserAgent = webview.getSettings().getUserAgentString();
}
});
return mUserAgent;
}
return ua;
}
}
(suppose you have mContext and mUserAgent in the field)

Categories

Resources