Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call WebView#getDrawingCache() asynchrnously without freezing UI thread

I'm trying to grab the screenshot of a WebView, however, WebView#getDrawingCache(true) is an expensive call and freezes the UI thread.

Nevertheless, any attempt to move this to an AsyncTask or a separate thread, will result in a "All WebView methods must be called on the UI thread" error.

Is there a workaround for this, such that I can work on WebView#getDrawingCache(true) in a background thread without freezing the UI thread?

Here's a some code example that crashes

public class TouchTask extends AsyncTask<Object, Void, Void> {
    Runnable completionCallback;

    @Override
    protected Void doInBackground(Object... objects) {
        Integer id = (Integer) objects[0];
        completionCallback = (Runnable) objects[1];
        touch(id);
        return null;
    }

    @Override
    protected void onPostExecute(Void res) {
        super.onPostExecute(res);
        completionCallback.run();
    }
}

/*
Updates the webview's bitmap in cache
 */
public Pair<String, Bitmap> touch(Integer id) {
    CustomWebView webView = getWebView(id);
    String title = webView.getTitle();
    Bitmap screenie = getScreenshotFromWebView(webView);

    Pair<String, Bitmap> res = new Pair<String, Bitmap>(title, screenie);
    bitmapCache.put(id, res);
    return res;
}

public void touchAsync(Integer id, Runnable callback) {
    new TouchTask().execute(id, callback);
}

public Bitmap getScreenshotFromWebView(WebView webView) {
    webView.setDrawingCacheEnabled(true);
    webView.buildDrawingCache(true);
    Bitmap screenieBitmap = null;
    if ((webView.getDrawingCache(true) != null) && (!webView.getDrawingCache(true).isRecycled())) {
        screenieBitmap = webView.getDrawingCache(false);
        screenieBitmap = compressScreenshot(webView.getDrawingCache(false));
    }
    return screenieBitmap;
}

public Bitmap compressScreenshot(Bitmap screenie) {
    Display display = activity.getWindowManager().getDefaultDisplay();
    Point size = new Point();
    display.getSize(size);
    int width = size.x;
    int height = size.y;
    return Bitmap.createScaledBitmap(screenie, width / 3, height / 3, true);
}

And here's the running code:

new TouchTask().execute(id, callback);

And here's the error

java.lang.RuntimeException: An error occured while executing doInBackground()
            at android.os.AsyncTask$3.done(AsyncTask.java:300)
            at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
            at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
            at java.util.concurrent.FutureTask.run(FutureTask.java:242)
            at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
            at java.lang.Thread.run(Thread.java:818)
     Caused by: java.lang.RuntimeException: java.lang.Throwable: A WebView method was called on thread 'AsyncTask #3'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {16dba051} called on null, FYI main Looper is Looper (main, tid 1) {16dba051})
            at android.webkit.WebView.checkThread(WebView.java:2185)
            at android.webkit.WebView.getTitle(WebView.java:1321)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager.touch(TabManager.java:53)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:145)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:138)
            at android.os.AsyncTask$2.call(AsyncTask.java:288)
            at java.util.concurrent.FutureTask.run(FutureTask.java:237)
            at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
            at java.lang.Thread.run(Thread.java:818)
     Caused by: java.lang.Throwable: A WebView method was called on thread 'AsyncTask #3'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {16dba051} called on null, FYI main Looper is Looper (main, tid 1) {16dba051})
            at android.webkit.WebView.checkThread(WebView.java:2175)
            at android.webkit.WebView.getTitle(WebView.java:1321)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager.touch(TabManager.java:53)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:145)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:138)
            at android.os.AsyncTask$2.call(AsyncTask.java:288)
            at java.util.concurrent.FutureTask.run(FutureTask.java:237)
            at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
            at java.lang.Thread.run(Thread.java:818)
like image 726
nubela Avatar asked Aug 09 '14 05:08

nubela


People also ask

What is WebView used for?

Android System WebView lets applications display browser windows in an app instead of transporting the user to another browser. Android developers use WebView when they want to display webpages in a Google app or other application.

What is WebView example?

In Android, WebView is a view used to display the web pages in application. This class is the basis upon which you can roll your own web browser or simply use it to display some online content within your Activity. We can also specify HTML string and can show it inside our application using a WebView.

What is a WebView URL?

WebView is a view that display web pages inside your application. You can also specify HTML string and can show it inside your application using WebView. WebView makes turns your application to a web application. In order to add WebView to your application, you have to add <WebView> element to your xml layout file.


1 Answers

To obtain a screenshot off a background thread, you'll have to use a different method.

We'll create a Canvas backed by a Bitmap and then ask the WebView, or the container holding the WebView(will discuss why you would need this later), to draw itself on this canvas. At the end, the Bitmap used by the Canvas will hold the required screenshot. It can then be flushed to storage.

I have modified your AsyncTask to represent the bare-bone implementation. Currently, it only shows that the Bitmap is being generated inside the doInBackground(...) method - off the main thread.

public class TouchTask extends AsyncTask<Object, Void, Bitmap> {

    @Override
    protected Bitmap doInBackground(Object... objects) {
        // Create a new Bitmap with dimensions of the WebView
        Bitmap b = Bitmap.createBitmap(webView.getWidth(), 
                            webView.getHeight(), Bitmap.Config.ARGB_8888);

        // Canvas that is backed by the `Bitmap` and used by webView to draw on
        Canvas c = new Canvas(b);

        // WebView draws itself
        webView.draw(c);

        // Return bitmap
        // OR: Compress the bitmap here itself
        // No need to do that in `onPostExecute(...)`
        return b;
    }

    // There's no need to actually do anything inside `onPostExecute(..)`
    // Compressing and saving the bitmap to sdcard can be handled
    // in `doInBackground(...)` itself
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);

        if (bitmap != null) {
            FileOutputStream fos;
            try {
                fos = new FileOutputStream(new File("/sdcard/webview_screenshot.png"));
                bitmap.compress(CompressFormat.PNG, 100, fos);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

While testing, I also found that this method takes less time than using getDrawingCache(). I haven't tried to find the reason yet.

...or the container holding the WebView(will discuss why you would need this later)...

Consider the scenario when the WebView is scrolled. If you use the method described above, the screenshot obtained will not be accurate - the screenshot will show the WebView as if the scroll was 0 - aligned top & left. To capture the WebView even in the scrolled state, place it inside a container - say Framelayout. The process remains similar, except for the following changes: we will use the FrameLayout's width and height when creating the Bitmap:

Bitmap b = Bitmap.createBitmap(frameLayout.getWidth(), 
                            frameLayout.getHeight(), Bitmap.Config.ARGB_8888);

Instead of asking the WebView, we'll ask the FrameLayout to draw itself:

frameLayout.draw(c);

And that should do it.

like image 104
Vikram Avatar answered Oct 02 '22 23:10

Vikram