In order to send an Authroization Header using my WebView I have custom WebViewRenderer and WebViewClient.
The Xamarin forms page with this Control can be created, loaded, closed and then recreated etc for various URLs without issue however after visiting a specific site the WebView will no longer load any URL with ShouldInterceptRequest not being called at all, even after closing and creating a new page with the control.
For example:
- Open MyWebViewPage to www.google.com. Success.
- Close MyWebViewPage
- Open MyWebViewPage to www.bing.com. Success.
- Close MyWebViewPage
- Open MyWebViewPage to mywebapp.local (the problem page) Success.
- Close MyWebViewPage
- Open MyWebViewPage to www.google.com. Fail, page does not render.
Without the CustomWebViewClient this issue does not ocurre (however I need this for the headers).
I am looking for either a direct fix to this issue or a way of completely reseting this control/page. A fix to the web site is not suitable as this may not always be possible.
Debug output of last successfull load (to problem site)
12-04 16:44:08.645 W/cr_AwContents(23188): onDetachedFromWindow called when already detached. Ignoring
12-04 16:44:08.653 I/cr_Ime (23188): ImeThread is not enabled.
12-04 16:44:08.889 W/cr_BindingManager(23188): Cannot call determinedVisibility() - never saw a connection for the pid: 23188
12-04 16:44:09.253 D/Mono (23188): GC_BRIDGE waiting for bridge processing to finish
12-04 16:44:09.257 I/art (23188): Starting a blocking GC Explicit
12-04 16:44:09.300 I/art (23188): Explicit concurrent mark sweep GC freed 6681(420KB) AllocSpace objects, 11(1400KB) LOS objects, 64% free, 4MB/12MB, paused 923us total 42.914ms
12-04 16:44:09.305 D/Mono (23188): GC_TAR_BRIDGE bridges 551 objects 9303 opaque 2855 colors 551 colors-bridged 551 colors-visible 551 xref 0 cache-hit 0 cache-semihit 0 cache-miss 0 setup 0.44ms tarjan 10.18ms scc-setup 0.29ms gather-xref 0.02ms xref-setup 0.03ms cleanup 0.63ms
12-04 16:44:09.305 D/Mono (23188): GC_BRIDGE: Complete, was running for 52.60ms
12-04 16:44:09.305 D/Mono (23188): GC_MINOR: (Nursery full) time 23.62ms, stw 24.64ms promoted 224K major size: 4048K in use: 3382K los size: 10240K in use: 8718K
Thread started: #17
12-04 16:44:10.867 D/Mono (23188): GC_BRIDGE waiting for bridge processing to finish
12-04 16:44:10.870 I/art (23188): Starting a blocking GC Explicit
12-04 16:44:10.913 I/art (23188): Explicit concurrent mark sweep GC freed 6338(406KB) AllocSpace objects, 31(3MB) LOS objects, 64% free, 4MB/12MB, paused 783us total 42.841ms
12-04 16:44:10.921 D/Mono (23188): GC_TAR_BRIDGE bridges 573 objects 599 opaque 26 colors 573 colors-bridged 573 colors-visible 573 xref 0 cache-hit 0 cache-semihit 0 cache-miss 0 setup 0.15ms tarjan 0.41ms scc-setup 0.39ms gather-xref 0.03ms xref-setup 0.05ms cleanup 0.28ms
12-04 16:44:10.921 D/Mono (23188): GC_BRIDGE: Complete, was running for 55.26ms
12-04 16:44:10.921 D/Mono (23188): GC_MINOR: (Nursery full) time 9.84ms, stw 11.16ms promoted 163K major size: 4224K in use: 3550K los size: 11264K in use: 9954K
[INFO:CONSOLE(21)] "JQMIGRATE: Logging is active", source: http://192.168.0.123/WebApp/Scripts/jquery-migrate-1.2.1.js (21)12-04 16:44:11.513 I/chromium(23188): [INFO:CONSOLE(21)] "JQMIGRATE: Logging is active", source: http://192.168.0.123/WebApp/Scripts/jquery-migrate-1.2.1.js (21)
[INFO:CONSOLE(41)] "JQMIGRATE: jQuery.fn.andSelf() replaced by jQuery.fn.addBack()", source: http://192.168.0.123/WebApp/Scripts/jquery-migrate-1.2.1.js (41)12-04 16:44:11.692 I/chromium(23188): [INFO:CONSOLE(41)] "JQMIGRATE: jQuery.fn.andSelf() replaced by jQuery.fn.addBack()", source: http://192.168.0.123/WebApp/Scripts/jquery-migrate-1.2.1.js (41)
[0:] Success
Debug output from subsequent faile load (to same or other site), blank page:
12-04 16:45:36.910 I/art (23188): Starting a blocking GC Explicit
12-04 16:45:36.941 I/art (23188): Explicit concurrent mark sweep GC freed 5227(440KB) AllocSpace objects, 24(3MB) LOS objects, 62% free, 4MB/12MB, paused 302us total 31.346ms
12-04 16:45:36.943 D/Mono (23188): GC_TAR_BRIDGE bridges 362 objects 8823 opaque 2742 colors 362 colors-bridged 362 colors-visible 362 xref 0 cache-hit 0 cache-semihit 0 cache-miss 0 setup 0.11ms tarjan 9.11ms scc-setup 0.23ms gather-xref 0.01ms xref-setup 0.02ms cleanup 0.28ms
12-04 16:45:36.943 D/Mono (23188): GC_BRIDGE: Complete, was running for 35.88ms
12-04 16:45:36.943 D/Mono (23188): GC_MINOR: (Nursery full) time 18.20ms, stw 20.86ms promoted 187K major size: 4400K in use: 3752K los size: 13432K in use: 11411K
12-04 16:45:36.973 W/cr_AwContents(23188): onDetachedFromWindow called when already detached. Ignoring
12-04 16:45:36.979 I/cr_Ime (23188): ImeThread is not enabled.
WebViewRenderer:
using Xamarin.Forms;
using MyApp.Droid;
[assembly: ExportRenderer(typeof(WebView), typeof(CustomWebViewRenderer))]
namespace MyApp.Droid
{
using Android.OS;
using global::Xamarin.Forms.Platform.Android;
using Android.Content;
using System;
public class CustomWebViewRenderer : WebViewRenderer
{
public CustomWebViewRenderer(Context context) : base(context)
{
}
public IWebViewController GetElementController()
{
return Element;
}
protected override void OnElementChanged(ElementChangedEventArgs<global::Xamarin.Forms.WebView> e)
{
try
{
base.OnElementChanged(e);
if (Control != null && e.NewElement != null)
{
SetupControlSettings();
}
}
catch (Exception ex)
{
Logging.LogException(ex);
}
}
private void SetupControlSettings()
{
try
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.JellyBeanMr1)
{
Control.Settings.MediaPlaybackRequiresUserGesture = false;
Control.Settings.JavaScriptEnabled = true;
Control.Settings.AllowContentAccess = true;
Control.Settings.DomStorageEnabled = true;
}
if ((int)Build.VERSION.SdkInt >= 19)
{
Control.SetLayerType(Android.Views.LayerType.Hardware, null);
}
else
{
Control.SetLayerType(Android.Views.LayerType.Software, null);
}
Control.Settings.SetAppCacheMaxSize(10 * 1024 * 1024);
Control.Settings.AllowFileAccess = true;
Control.Settings.AllowUniversalAccessFromFileURLs = true;
Control.Settings.SetAppCacheEnabled(true);
Control.Settings.CacheMode = Android.Webkit.CacheModes.NoCache;
var webViewClient = new CustomWebViewClient(this, MyApp.GlobalVariables.AuthToken);
Control.SetWebViewClient(webViewClient);
}
catch (Exception ex)
{
Logging.LogException(ex);
}
}
}
}
WebViewClient:
using Android.Webkit;
using System;
using System.Net.Http;
using Xamarin.Forms;
namespace MyApp.Droid
{
public class CustomWebViewClient : WebViewClient
{
private readonly string authToken;
private readonly CustomWebViewRenderer renderer;
public CustomWebViewClient(CustomWebViewRenderer renderer, string authToken)
{
this.renderer = renderer;
this.authToken = authToken;
}
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, IWebResourceRequest request)
{
WebNavigatingEventArgs args = new WebNavigatingEventArgs(WebNavigationEvent.NewPage, new UrlWebViewSource { Url = request.Url.ToString() }, request.Url.ToString());
renderer.GetElementController().SendNavigating(args);
if (args.Cancel)
{
view.StopLoading();
}
return base.ShouldOverrideUrlLoading(view, request);
}
public override WebResourceResponse ShouldInterceptRequest(Android.Webkit.WebView view, IWebResourceRequest request)
{
try
{
if (!string.IsNullOrWhiteSpace(authToken))
{
string authorizationValue = "Bearer " + authToken;
using (HttpClient client = new HttpClient())
{
request.RequestHeaders.Add("Authorization", authorizationValue);
foreach(var h in request.RequestHeaders)
{
if (client.DefaultRequestHeaders.Contains(h.Key))
{
client.DefaultRequestHeaders.Remove(h.Key);
}
client.DefaultRequestHeaders.Add(h.Key,h.Value);
}
HttpResponseMessage response = client.GetAsync(request.Url.ToString()).Result;
string encoding = response.Content.Headers.ContentEncoding.GetEnumerator().Current;
string contentType = response.Content.Headers.ContentType.ToString();
System.IO.Stream stream = response.Content.ReadAsStreamAsync().Result;
if (string.IsNullOrWhiteSpace(encoding) && contentType.Contains(";"))
{
string[] parts = contentType.Split(';');
contentType = parts[0].Trim();
encoding = parts[1].Trim();
}
WebResourceResponse result = new WebResourceResponse(contentType, encoding, stream);
return result;
}
}
}
catch (Exception ex)
{
Logging.LogException(ex);
}
return base.ShouldInterceptRequest(view, request);
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
try
{
base.OnPageFinished(view, url);
UrlWebViewSource source = new UrlWebViewSource { Url = url };
WebNavigatedEventArgs args = new WebNavigatedEventArgs(WebNavigationEvent.NewPage, source, url, WebNavigationResult.Success);
renderer?.GetElementController()?.SendNavigated(args);
}
catch (Exception ex)
{
Logging.LogException(ex);
}
}
}
}
EDIT 1: It seems to be due to this code:
HttpResponseMessage response = client.GetAsync(request.Url.ToString()).Result;
If I comment out the WebResourceResponse return (so it falls down to the base method) but leave this in the issue is still present, but commenting this line our makes it work.
EDIT 2: The plot thickens. The problem site uses SignalR for event notifications. After adding some logging I can see that the WebViewClient starts to load .../signalr/connect?transport=serverSentEvents&connectionToken= but never finishes, I'm guessing this is how SignalR works with the response only coming after an event on the server. This is however blocking subsequent calls to the WebViewClient