ABYX

Make a WebView in Android that caches images

For our newest app, Android Police Reader, I was working on a WebView in Android that stores all rendered images offline. This is something many people wan't to do, but there's very little information on the subject, so here I'm going to describe the process of making a CachedWebView.

WebViewClients

There are 2 fundamental WebViewClient's in Android. One is the WebChromeClient and one is the WebViewClient. Both work, but have different purposes. We're going to use the WebViewClient because this one has a method called shouldInterceptRequest() that processes all resources that should be loaded in the WebView. What we're going to do is basically extend the WebViewClient class and override the shouldInterceptRequest() method. So, we get the following code.

public class CachedWebViewClient extends WebViewClient {
    private String[] imageExtensions = {"jpg", "jpeg", "png", "gif", "JPG", "JPEG", "PNG", "GIF"};
    private Context context;

    /**
     * Create a new CachedWebViewClient that saves images to a custom location so they can be used
     * again later on.
     */
    public CachedWebViewClient(Context context) {
        this.context = context;
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        return null;
    }

    /**
     * Checks whether a given url points to a valid image. The following formats are treated as
     * images: png, jpg (and jpeg) and gif (Also in capital letters).
     *
     * @param url Url that points at a certain location on the web. Must be different from null.
     * @return true when url points to a valid image.
     */
    private boolean isImage(String url){
        for (String extension: imageExtensions){
            if (url.endsWith(extension)){
                return true;
            }
        }
        return false;
    }
}

The isImage() method uses an array of image extensions to determine if a given url points to an image resource or not. The original WebViewClient will process the request itself when null is returned in the shouldInterceptRequest() method, but when you return a WebResourceResponse you basically tell the WebViewClient that you're handling the request yourself. A WebResourceResponse takes a mime type, an encoding type and an InputStream. We'll be filling these in later on.

Implementing the shouldInterceptRequest() method

Now, we're going to implement the shouldInterceptRequest method step-by-step. First we need to get the url that points to the resource that's being loaded. This is embedded in the request argument and can be retrieved by doing request.getURL(). The full path is then:

Uri source = request.getUrl();
String url = "http://androidpolice.com" + source.getPath();

Then we have to check if source.getPath() is different from null, because otherwise our application would crash later on. After that the caching process starts. Note that we are not using an AsyncTask here to connect to the web. This is not necessary because the shouldInterceptRequest() method is already running on a seperate thread. The rest of our implementation looks like this:

 @Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        Uri source = request.getUrl();
        String url = "http://androidpolice.com" + source.getPath();
        if (source.getPath() != null) {
            try {
                if (isImage(url)) {
                    // Check if image is present in memory.
                    File file = new File(context.getCacheDir().getAbsolutePath() + "/" + source.getLastPathSegment());
                    if (!file.exists()) {
                        // File is not present in cache and thus needs to be downloaded
                        InputStream in = new java.net.URL(url).openStream();
                        Bitmap image = BitmapFactory.decodeStream(in);
                        FileOutputStream fos = new FileOutputStream(new File(context.getCacheDir().getAbsolutePath() + "/" + source.getLastPathSegment()));
                        image.compress(Bitmap.CompressFormat.PNG, 100, fos);
                    }
                    FileInputStream fis = new FileInputStream(new File(context.getCacheDir().getAbsolutePath() + "/" + source.getLastPathSegment()));
                    return new WebResourceResponse("image/jpeg", "utf-8", fis);
                } else {
                    // Delegate image loading completely to parent
                    return null;
                }
            } catch (MalformedURLException e) {
                showToastWithHandler("Malformed URL while loading image!");
            }  catch (IOException e) {
                showToastWithHandler("Unknown IO-error occurred! Try again...");
            }
        }
        return null;
    }

We first make a new File object that points to a location in Android's internal cache. The path to the internal cache can be found with getCacheDir(). You can use another location to store your images, but we're choosing for the internal cache because this one's is managed by Android itself and can be cleared by the OS when it's running low on internal storage. After the File object has been created, we check if it already exists or not. We only have to create the image, if it hasn't been downloaded already. Then we open a new connection to the image on the web and download it with these 4 statements:

InputStream in = new java.net.URL(url).openStream();
Bitmap image = BitmapFactory.decodeStream(in);
FileOutputStream fos = new FileOutputStream(new File(context.getCacheDir().getAbsolutePath() + "/" + source.getLastPathSegment()));
image.compress(Bitmap.CompressFormat.PNG, 100, fos);

We're storing the file as a PNG, because that delivers the best quality and retains transparent layers, but you can choose another format such as JPG if you like. As last, we're opening an InputStream that we can return and that can be used by the parent to render the resource. The showToastWithHandler() is a method I made myself to display error messages on the UI-thread. Full source code is given below:

/**
 * Class which extends the WebChromeClient and stores all images present in an article in persistent
 * memory to enable offline usage.
 *
 * @author Pieter Verschaffelt
 */
public class CachedWebViewClient extends WebViewClient {
    private String[] imageExtensions = {"jpg", "jpeg", "png", "gif", "JPG", "JPEG", "PNG", "GIF"};
    private Context context;

    /**
     * Create a new CachedWebViewClient that saves images to a custom location so they can be used
     * again later on.
     */
    public CachedWebViewClient(Context context) {
        this.context = context;
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        Uri source = request.getUrl();
        String url = "http://androidpolice.com" + source.getPath();
        if (source.getPath() != null) {
            try {
                if (isImage(url)) {
                    System.out.println("File is: " + url);
                    // Check if image is present in memory.
                    File file = new File(context.getCacheDir().getAbsolutePath() + "/" + source.getLastPathSegment());
                    if (!file.exists()) {
                        // File is not present in cache and thus needs to be downloaded
                        InputStream in = new java.net.URL(url).openStream();
                        Bitmap image = BitmapFactory.decodeStream(in);
                        FileOutputStream fos = new FileOutputStream(new File(context.getCacheDir().getAbsolutePath() + "/" + source.getLastPathSegment()));
                        image.compress(Bitmap.CompressFormat.PNG, 100, fos);
                    }
                    FileInputStream fis = new FileInputStream(new File(context.getCacheDir().getAbsolutePath() + "/" + source.getLastPathSegment()));
                    return new WebResourceResponse("image/jpeg", "utf-8", fis);
                } else {
                    // Delegate image loading completely to parent
                    return null;
                }
            } catch (MalformedURLException e) {
                showToastWithHandler("Malformed URL while loading image!");
            } catch (FileNotFoundException e) {
                showToastWithHandler("Image not found in persistent memory! Remove app data and try again...");
            } catch (IOException e) {
                showToastWithHandler("Unknown IO-error occurred! Try again...");
            }
        }
        return null;
    }

    /**
     * Show a toast on the main UI-thread using a Handler. This method can be called from a non-
     * UI-thread and will still display the message.
     *
     * @param message text to be shown in toast
     */
    private void showToastWithHandler(final String message){
        Handler mainHandler = new Handler(context.getMainLooper());
        final Utils utils = new Utils();
        mainHandler.post(new Runnable() {
            @Override
            public void run() {
                utils.showToast(message, Toast.LENGTH_LONG, context);
            }
        });
    }

    /**
     * Checks whether a given url points to a valid image. The following formats are treated as
     * images: png, jpg (and jpeg) and gif (Also in capital letters).
     *
     * @param url Url that points at a certain location on the web. Must be different from null.
     * @return true when url points to a valid image.
     */
    private boolean isImage(String url){
        for (String extension: imageExtensions){
            if (url.endsWith(extension)){
                return true;
            }
        }
        return false;
    }
}

Post created by Pieter Verschaffelt on 2016-07-13 10:20:18