iam trying to build a class to get any data from webiew (this code is collected from many posts),
i need a function to do the same as "stringByEvaluatingJavascriptFromString" in iPhone
public class JavaScriptInterface
{
String RetValue = "";
public String ExecuteScript(WebView webv, String expression)
{
RetValue = "";
for (int t=0;t<2;t++) // try loadUrl twice (incase the first call fails)
{
webv.loadUrl("javascript:var FunctionOne = function () { var res = " + expression + "; window.HTMLOUT.SendCallback(res); }; FunctionOne();");
for (int i = 0; i < 35; i++) { // wait 3.5 seconds
try {
sleep(100);
} catch (Exception exp) {
}
if (RetValue != "") return RetValue; // if the browser returned a value return this value
}
}
return RetValue; // if the loadUrl fails, the return value is empty string
}
#JavascriptInterface
public void SendCallback(String jsResult)
{
RetValue = jsResult;
}
}
This function works most of times,
however, sometimes there is no return value (just like the script fails or something i dont know why)
is there a way to fix this code?
thank you.
I am posting the answer in case someone has the same problem,
after days of searching i have successfully solved my problem:
1 - WebView init code should be like this
webv = (WebView) findViewById(R.id.webv);
webv.getSettings().setJavaScriptEnabled(true);
webv.setWebChromeClient(new WebChromeClient());
webv.setWebViewClient(new WebViewClient() { });
javaInterface = new JavaScriptInterface();
webv.addJavascriptInterface(javaInterface, "HTMLOUT");
2 - Do not add "setDomStorageEnabled" i dont know why it causes problems to the code above
3 - i was using this code for javascript alerts, but it seems that it causes problems too, so i had to remove it.
webv.setWebChromeClient(new WebChromeClient()
{
#Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
});
Now it is working better, however, sometimes i have to call the ExecuteScript multiple times to get the result.
i hope someone can explain this problem.
Related
I'm trying to display a PDF on Android in a Xamarin.Forms project and it works fine, except for the first time it's loaded where just one blank page appears 9 times out of 10.
The first call is to this function, located in the Android project:
public string HTMLToPDF(string html, string filename) {
//html param is a full html description of the pdf
//filename param is something like "example.pdf"
try {
var dir = new Java.IO.File(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath + "/folder/");
var file = new Java.IO.File(dir + "/" + filename);
if (!dir.Exists())
dir.Mkdirs();
int x = 0;
while (file.Exists())
{
x++;
file = new Java.IO.File(dir + "/" + filename + "( " + x + " )");
}
if (webpage == null)
webpage = new Android.Webkit.WebView(Android.App.Application.Context);
else
webpage.RemoveAllViews();
int width = 2100;
int height = 2970;
webpage.Layout(0, 0, width, height);
webpage.LoadData(html, "text/html", "UTF-8");
webpage.SetWebViewClient(new WebViewCallBack(file.ToString()));
this.Print(webpage, file.ToString(), filename);
return file.ToString();
}
catch (Java.Lang.Exception e)
{
App._mainPage.DisplayAlert("Error", e.Message, "Ok");
}
return "";
}
This is what the WebViewCallBack class looks like:
class WebViewCallBack : Android.Webkit.WebViewClient
{
string fileNameWithPath = null;
public WebViewCallBack(string path)
{
this.fileNameWithPath = path;
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
view.SetInitialScale(1);
view.Settings.LoadWithOverviewMode = true;
view.Settings.UseWideViewPort = true;
PdfDocument document = new Android.Graphics.Pdf.PdfDocument();
Android.Graphics.Pdf.PdfDocument.Page page = document.StartPage(new Android.Graphics.Pdf.PdfDocument.PageInfo.Builder(2100, 2970, 1).Create());
view.Draw(page.Canvas);
document.FinishPage(page);
Stream filestream = new MemoryStream();
Java.IO.FileOutputStream fos = new Java.IO.FileOutputStream(fileNameWithPath, false);
try
{
document.WriteTo(filestream);
fos.Write(((MemoryStream)filestream).ToArray(), 0, (int)filestream.Length);
fos.Close();
}
catch (Java.Lang.Exception e)
{
App._mainPage.DisplayAlert("Erreur", e.Message, "Ok");
}
}
}
And the method Print called at the end:
public void Print(Android.Webkit.WebView webView, string filename, string onlyFileName)
{
try
{
PrintAttributes.Builder builder = new PrintAttributes.Builder();
PrintAttributes.Margins margins = new PrintAttributes.Margins(0, 0, 0, 80);
builder.SetMinMargins(margins);
builder.SetMediaSize(PrintAttributes.MediaSize.IsoA4);
builder.SetColorMode(PrintColorMode.Color);
PrintAttributes attr = builder.Build();
PrintManager printManager = (PrintManager)Forms.Context.GetSystemService(Android.Content.Context.PrintService);
var printAdapter = new GenericPrintAdapter(Forms.Context, webView, filename, onlyFileName);
printAdapter.OnEnded += PrintAdapter_OnEnded;
printAdapter.OnError += PrintAdapter_OnError;
printManager.Print(filename, printAdapter, attr);
}
catch (Java.Lang.Exception e)
{
App._mainPage.DisplayAlert("Erreur", e.Message, "Ok");
}
}
When I put a breakpoint on the first line of the OnPageFinished callback of the WebViewCallBack class, I see two different things at this point:
either the PDF interface is up but it's still loading the PDF. In that case, the PDF loads fine once I click on "play" again.
either the PDF has already loaded, as a single blank page. This only happens when it's the first try at loading this particular PDF.
Thus I guess I have to find a way to force the loader to wait for the OnPageFinished method to run first? But that seems wrong.
I can also add that the original HTML contains images, which are all appearing as base64 string in the html string I'm feeding to HTMLToPDF. I noticed that the PDF loads well even on the first try if there are no images in the HTML, so I thought the problem might be that the PDF loads before it's ready on the first try only, maybe because of the images. I couldn't find a fix for that though.
Can anybody shed some light on this for me?
So there are a couple of ways to handle the flow, but I would go with just an event or an Observable.
So lets convert your WebViewCallBack to something that actually performs a callback when its OnPageFinished is called. This is using System.Reactive but you could also use an EventHandler...
// a few class level variables
bool busy;
WebView webView;
IDisposable WhenPageIsLoadedSubscription;
public class WebViewObservable : WebViewClient
{
Subject<string> pageLoaded = new Subject<string>();
public IObservable<string> WhenPageIsLoaded
{
get { return pageLoaded.AsObservable(); }
}
public override void OnPageFinished(WebView view, string url)
{
pageLoaded.OnNext(url);
}
}
Now define your print routine (the calls in your original OnPageFinished and Print method). This will automatically be called when the webpage is finished loading.
void PrintWebPage(WebView webView, string url)
{
Log.Debug("SO", $"Page: {url} loaded, lets print it now" );
// Perform the work that you used to do in OnPageFinished
// Perform the work in your Print method
// Now turn off a Progress indictor if desired
busy = false;
}
Setup your WebView with the observable that calls the PrintWebPage action every time a page is loaded...
void LoadAndPrintWebPage(string html))
{
busy = true;
if (webView == null)
{
int width = 2100;
int height = 2970;
webView = new WebView(this);
var client = new WebViewObservable();
WhenPageIsLoadedSubscription = client.WhenPageIsLoaded.Subscribe((url) => { PrintWebPage(webView, url); });
webView.SetWebViewClient(client);
webView.Layout(0, 0, width, height);
}
webView.LoadData(html, "text/html", "UTF-8");
}
Now call LoadWebPage with your html content and it will be automatically printed after the page is finished loading...
LoadAndPrintWebPage(html);
When you are done, clean up your observable and webview to avoid memory leaks...
void CleanupWebView()
{
WhenPageIsLoadedSubscription?.Dispose();
webView?.Dispose();
webView = null;
}
I had similar problem with blank page. But I used google doc for displaying it. http://docs.google.com/gview?embedded=true&url=
I could solved my problem only by checking operation time between OnPageStarted and OnPageFinished. If it took short time, then something go wrong and page is blank.
here is my webClient.
Every time then page is blank I just reload it. Also add simple circuit breaker just in case
public class MyWebViewClient : WebViewClient
{
private readonly WebView _webView;
private DateTime _dateTime = DateTime.Now;
private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1);
private int _breakerHits;
private const int _BreakerCount = 2;
public MyWebViewClient(WebView webView)
{
_webView = webView;
}
public override async void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
{
await _semaphoreSlim.WaitAsync();
_dateTime = DateTime.Now;
base.OnPageStarted(view, url, favicon);
_webView.SendNavigating(new WebNavigatingEventArgs(WebNavigationEvent.NewPage, null, url));
_semaphoreSlim.Release();
}
public override async void OnPageFinished(Android.Webkit.WebView view, string url)
{
await _semaphoreSlim.WaitAsync();
base.OnPageFinished(view, url);
if (url.Contains(".pdf"))
{
var diff = DateTime.Now - _dateTime;
if (diff > TimeSpan.FromMilliseconds(700) || _breakerHits > _BreakerCount)
{
_breakerHits = 0;
_webView.SendNavigated(new WebNavigatedEventArgs(WebNavigationEvent.NewPage, null, url,
WebNavigationResult.Success));
}
else
{
_breakerHits++;
view.Reload();
}
}
else
{
_webView.SendNavigated(new WebNavigatedEventArgs(WebNavigationEvent.NewPage, null, url,
WebNavigationResult.Success));
}
_semaphoreSlim.Release();
}
}
I am working on a web Browser, I have a SearchView in it, for user to input queries. I want to differentiate between a search query or a web address. My current code just add http://www. in front of any query that comes in and try to load it.
This is my current code.
String query = search_q;
if(!query.startsWith("www.")&& !query.startsWith("http://")){
query = "www."+ query ;
}
if(!query.startsWith("http://")){
query = "http://"+query;
}
if( Patterns.WEB_URL.matcher(query).matches()){ //checks if the query looks like an URL
web1.loadUrl(query);
}
else
web1.loadUrl("https://www.google.com//search?q=+"+search_q);
The problem is that Patterns.WEB_URL.matcher(query).matches() returns true even if http://www.abc is passed into it.
check
if( Patterns.WEB_URL.matcher(query).matches() && isUrl(query)){ //checks if the query looks like an URL
web1.loadUrl(query);
}
function defintion isUrl:-
public Boolean isUrl(String query){
int a=0;
int onlyfind=0;
for (int i = 0 ; i<query.length() ; i++){
if (query.charAt(i) == '.')
a++;
if(a==1){
onlyfind= i;
}
}
if(a==1){
if(query.substring(0,onlyfind+1).equals("http://www"))
return false;
}
return true;
}
Try validating the url(query) using this method:
//isValidURL(query);
boolean isValidURL(String url) {
try {
new URI(url).parseServerAuthority();
return true;
} catch (URISyntaxException e) {
return false;
}
}
If it returns false, then:
web1.loadUrl("https://www.google.com//search?q=+"+search_q);
I think the best way to validate URL is using regex. Because if you are working on a browser, you should not limit the validation of string queries that starts with www, what if the website is dev.website.com? it is a valid website URL, or what if the address starts with https and not just http?. So I think Try using regex to have a reference pattern in validating if the query string is URL or not:
private boolean isURL(CharSequence searchQuery) {
Pattern urlPattern = Pattern.compile("(https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\w\\D\\S]{2,}|[^\\s]{2,}[a-zA-Z0-9]\\.[^\\s]{2,})");
Matcher matcher = urlPattern.matcher(searchQuery);
return matcher.matches();
}
Then call that method:
String query = search_q;
if(isURL(query)) {
web1.loadUrl(query);
} else {
web1.loadUrl("https://www.google.com//search?q=+"+search_q);
}
Hope this helps.
I have a WebView with custom WebViewClient class, which allowed me to react to user's clicks on links inside the WebView and process them.
Here's the code:
class WebViewClickClient extends WebViewClient {
#Override
public void onPageFinished(WebView view, String url) {
String note = "";
String note_id = new String(url);
if (note_id.contains("#")) {
int offset = note_id.indexOf("#");
note_id = note_id.substring(offset + 1, note_id.length());
}
for (int i = 0; i < bookNotes.size(); i++) {
if (bookNotes.get(i).noteId().equals(note_id)) {
note = bookNotes.get(i).noteSection();
break;
}
}
if (!note.isEmpty()) {
Toast.makeText(getContext(), note, LENGTH_LONG).show();
}
}
Now it's not working anymore and the WebViewClient's methods aren't even triggered upon click. Instead I get this message: I/chromium: [INFO:CONSOLE(0)] "Not allowed to navigate top frame to data URL:"
Do I need to handle this with JavaScript now or is there any workaround with WebView for Android?
UPDATE:
For anyone interested in workaround with JS Interface here's how I made it.
The logic for reacting to user clicks moved from the onPageFinished method to
private class JSInterface {
public void showToast(String key) {...}
}
Then, WebView is created as follows:
WebView book_view;
book_view = (WebView) view.findViewById(R.id.web_text_view);
book_view.getSettings().setJavaScriptEnabled(true);
book_view.addJavascriptInterface(new JSInterface(), "JAVA_CODE");
JAVA_CODE object will be called from JavaScript code which has to be added dynamically to each WebView "page" by concatenating text contents with JS and calling loadData() method on the WebView.
StringBuilder j_script = new StringBuilder();
if (note_ids.size() > 0) {
j_script.append("<script type=\"text/javascript\">\nfunction jscallback()\n{");
for (int i = 0; i < note_ids.size(); i++) {
j_script.append("var dv" + i + " = document.getElementById(\"" + note_ids.get(i) + "\");\ndv" + i + ".onclick = function() {JAVA_CODE.showToast(\"" + note_ids.get(i) + "\")};\n");
}
j_script.append("}</script>");
}
I've seen several questions on this but all of the solutions didn't work for me. For a client we have to develop an app that actually does nothing except of showing a WebView and a native DrawerLayout. However, we only have their mobile webpage (with a menu, etc.). So we have to hide some elements. It is very important that the existing stylesheets stay the same, just some other CSS rules are added.
What I've tried so far:
#Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (!mErrorOccured && !noConnectionAvailable) {
injectCSS();
}
mainActivity.hideLoadingScreen();
}
With this Injection:
// Inject CSS method: read style.css/readmode.css from assets folder
// Append stylesheet to document head
private void injectCSS() {
try {
Activity activity = (Activity) mContext;
SharedPreferences sharPref = activity.getSharedPreferences(Constants.PREFERENCE_NAME, Context.MODE_PRIVATE);
Boolean isReadMode = sharPref.getBoolean(Constants.READMODE_KEY, false);
InputStream inputStream;
if (isReadMode) {
inputStream = activity.getAssets().open("readmode.css");
} else {
inputStream = activity.getAssets().open("style.css");
}
byte[] buffer = new byte[inputStream.available()];
inputStream.read(buffer);
inputStream.close();
String encoded = Base64.encodeToString(buffer, Base64.NO_WRAP);
webView.loadUrl("javascript:(function() {" +
"var parent = document.getElementsByTagName('head').item(0);" +
"var style = document.createElement('style');" +
"style.type = 'text/css';" +
// Tell the browser to BASE64-decode the string into your script !!!
"style.innerHTML = window.atob('" + encoded + "');" +
"parent.appendChild(style);" +
"Android.injectCSS('Works!');})()");
} catch (Exception e) {
e.printStackTrace();
}
}
I've also tried to add a JavaScript Interface that uses the Android.injectCSS('Works!'); of the JavaScript above combined with:
#JavascriptInterface
public void injectCSS(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_LONG).show();
mMainActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
mMainFragment.unhideWebView();
}
});
}
And:
public void unhideWebView() {
webView.setVisibility(View.VISIBLE);
}
However, there is always a delay before the elements of the web page are hidden. I've also tried to use Jsoup. First this threw an NetworkOnMainThreadException. After I've tried to use it with an AsyncTask, it was not possible to change the WebView on onPostExecute() because this handling must be on the main thread. Even using a runOnUiThrad() did not help calling loadData() on the WebView with the new loaded data.
Is there any way to inject CSS/JS before the WebView shows up?
I have the following code to make requests to a REST API, using Xamarin and an Android device:
public class ApiBase
{
HttpClient m_HttpClient;
public ApiBase(string baseAddress, string username, string password)
{
if (!baseAddress.EndsWith("/"))
{
baseAddress += "/";
}
var handler = new HttpClientHandler();
if (handler.SupportsAutomaticDecompression)
{
handler.AutomaticDecompression = DecompressionMethods.GZip;
}
m_HttpClient = new HttpClient(handler);
m_HttpClient.BaseAddress = new Uri(baseAddress);
var credentialsString = Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + password));
m_HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentialsString);
m_HttpClient.Timeout = new TimeSpan(0, 0, 30);
}
protected async Task<XElement> HttpGetAsync(string method)
{
try
{
HttpResponseMessage response = await m_HttpClient.GetAsync(method);
if (response.IsSuccessStatusCode)
{
// the request was successful, parse the returned string as xml and return the XElement
var xml = await response.Content.ReadAsAsync<XElement>();
return xml;
}
// the request was not successful -> return null
else
{
return null;
}
}
// some exception occured -> return null
catch (Exception)
{
return null;
}
}
}
If i have it like this, the first and the second call to HttpGetAsync work perfectly, but from the 3rd on the GetAsyncstalls and eventually throws an exception due to the timeout. I send these calls consecutively, there are not 2 of them running simultaneously since the results of the previous call are needed to decide the next call.
I tried using the app Packet Capture to look at the requests and responses to find out if i'm sending an incorrect request. But it looks like the request which fails in the end is never even sent.
Through experimentation i found out that everything works fine if don't set the AutomaticDecompression.
It also works fine if i change the HttpGetAsync method to this:
protected async Task<XElement> HttpGetAsync(string method)
{
try
{
// send the request
var response = await m_HttpClient.GetStringAsync(method);
if (string.IsNullOrEmpty(response))
{
return null;
}
var xml = XElement.Parse(response);
return xml;
}
// some exception occured -> return null
catch (Exception)
{
return null;
}
}
So basically using i'm m_HttpClient.GetStringAsync instead of m_HttpClient.GetAsync and then change the fluff around it to work with the different return type. If i do it like this, everything works without any problems.
Does anyone have an idea why GetAsync doesn't work properly (doesn't seem to send the 3rd request) with AutomaticDecompression, where as GetStringAsync works flawlessly?
There are bug reports about this exact issue:
https://bugzilla.xamarin.com/show_bug.cgi?id=21477
The bug is marked as RESOLVED FIXED and the recomended action is to update to the latest stable build. But there are other (newer) bugreports that indicate the same thing that are still open, ex:
https://bugzilla.xamarin.com/show_bug.cgi?id=34747
I made a workaround by implementing my own HttpHandler like so:
public class DecompressionHttpClientHandler : HttpClientHandler
{
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.AcceptEncoding.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("gzip"));
var msg = await base.SendAsync(request, cancellationToken);
if (msg.Content.Headers.ContentEncoding.Contains("gzip"))
{
var compressedStream = await msg.Content.ReadAsStreamAsync();
var uncompresedStream = new System.IO.Compression.GZipStream(compressedStream, System.IO.Compression.CompressionMode.Decompress);
msg.Content = new StreamContent(uncompresedStream);
}
return msg;
}
}
Note that the code above is just an example and not a final solution. For example the request will not be compressed and all headers will be striped from the result. But you get the idea.