okonjin
okonjin

Reputation: 151

PDF displays empty the first time it's loaded on Android

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:

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?

Upvotes: 0

Views: 1166

Answers (2)

Dmitry Zinoviev
Dmitry Zinoviev

Reputation: 588

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();

        }
    }

Upvotes: 0

SushiHangover
SushiHangover

Reputation: 74209

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;
}

Upvotes: 1

Related Questions