Saving the Android WebView cache on the SD card

Source code

It could be useful to save the cache of an Android WebView on the SD card (or integrated external memory) especially for devices with a limited amount of internal memory, but how can you do that? Well, it’s simple. You know that some Android browsers already do it and here I’m going to tell you a way to do it in your own app as well. This solution is made to work with Android 2.1 and higher, so I’m not going to use the API that was introduced only in a later version of the OS.

The key is the getCacheDir method of the ContextWrapper class. This is what is used by the cache manager to decide where to store the cache files. The behavior is slightly different for Android 2.1 and Android 2.2 and higher, so we’ll also keep this in mind.

I’ll explain everything going through the source code of the example application that you can download through the link on top of this post. Let’s start with the mainfest file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.devahead.androidwebviewcacheonsd"
	android:versionCode="1"
	android:versionName="1.0">

	<uses-sdk android:minSdkVersion="7"/>
	
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
	<uses-permission android:name="android.permission.INTERNET"/>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
		
	<application
		android:icon="@drawable/ic_launcher"
		android:label="@string/app_name"
		android:name="com.devahead.androidwebviewcacheonsd.ApplicationExt">
		<activity
			android:name=".MainActivity"
			android:label="@string/app_name">
			<intent-filter>
				<action android:name="android.intent.action.MAIN"/>
				<category android:name="android.intent.category.LAUNCHER"/>
			</intent-filter>
		</activity>
		<activity
			android:name=".SecondActivity"
			android:label="Second activity"/>
	</application>

</manifest>

Here we have the permissions to write to the SD (the external storage) and access the web. We also have a custom Application class extension called ApplicationExt and a couple of activities, MainActivity and SecondActivity.

The ApplicationExt class is where most of the things are done to have the cache on the SD:

package com.devahead.androidwebviewcacheonsd;

import java.io.File;

import android.app.Application;
import android.os.Environment;

public class ApplicationExt extends Application
{
	// NOTE: the content of this path will be deleted
	//       when the application is uninstalled (Android 2.2 and higher)
	protected File extStorageAppBasePath;
	
	protected File extStorageAppCachePath;

	@Override
	public void onCreate()
	{
		super.onCreate();

		// Check if the external storage is writeable
		if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
		{
			// Retrieve the base path for the application in the external storage
			File externalStorageDir = Environment.getExternalStorageDirectory();
				
			if (externalStorageDir != null)
			{
				// {SD_PATH}/Android/data/com.devahead.androidwebviewcacheonsd
				extStorageAppBasePath = new File(externalStorageDir.getAbsolutePath() +
					File.separator + "Android" + File.separator + "data" +
					File.separator + getPackageName());
			}
	
			if (extStorageAppBasePath != null)
			{
				// {SD_PATH}/Android/data/com.devahead.androidwebviewcacheonsd/cache
				extStorageAppCachePath = new File(extStorageAppBasePath.getAbsolutePath() +
					File.separator + "cache");
	
				boolean isCachePathAvailable = true;
				
				if (!extStorageAppCachePath.exists())
				{
					// Create the cache path on the external storage
					isCachePathAvailable = extStorageAppCachePath.mkdirs();
				}
				
				if (!isCachePathAvailable)
				{
					// Unable to create the cache path
					extStorageAppCachePath = null;
				}
			}
		}
	}
	
	@Override
	public File getCacheDir()
	{
		// NOTE: this method is used in Android 2.2 and higher
		
		if (extStorageAppCachePath != null)
		{
			// Use the external storage for the cache
			return extStorageAppCachePath;
		}
		else
		{
			// /data/data/com.devahead.androidwebviewcacheonsd/cache
			return super.getCacheDir();
		}
	}
}

In the onCreate method we start by checking if the external storage is actually mounted and we can write on it, then we build the base path of the app on the external storage. This path is {SD_PATH}/Android/data/com.devahead.androidwebviewcacheonsd because com.devahead.androidwebviewcacheonsd is the package declared in the manifest file and using this path structure makes sure that the entire path and all its content will be automatically deleted by Android 2.2 and higher in case the app is uninstalled avoiding to have garbage files on the SD. Note that this works only in Android 2.2 and higher, while in Android 2.1 the path will not be deleted by the OS so you’ll have to deal with it on your own. The full path for the cache files will be {SD_PATH}/Android/data/com.devahead.androidwebviewcacheonsd/cache, so the base path plus the cache directory. We must also make sure that the path is actually available before using it, so we create all the directories with the mkdirs method in case they don’t already exist.

Now that we have the cache path on the SD, we’re ready to use it and all we have to do is override the getCacheDir method inside our ApplicationExt class. This method is invoked by the cache manager when the application is started so basically we’re saying that the cache will be stored on the SD if it’s available and writeable, while we use the default path in case we’re allowed to use only the internal memory. Android stores all the cache data for our app in the /data/data/com.devahead.androidwebviewcacheonsd/cache directory by default on the internal memory.

We’re done with the implementation for Android 2.2 and higher, but what about Android 2.1? For that version of the OS the cache manager doesn’t use the getCacheDir method of the Application context, but it uses the one of the Activity context instead. So to make our solution work also with Android 2.1, we must override the getCacheDir method inside our activities.

To immediately understand how it works with Android 2.1, let’s take a look at the activities of the example application. We start with the MainActivity:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:orientation="vertical">

  <Button android:id="@+id/startSecondActivityBtn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="Start second activity"/>

	<WebView android:id="@+id/webView"
		android:layout_width="fill_parent"
		android:layout_height="fill_parent"/>

</LinearLayout>

There’s simply a button to start the SecondActivity and a WebView to test our solution. The code for MainActivity looks like this:

package com.devahead.androidwebviewcacheonsd;

import java.io.File;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;

public class MainActivity extends Activity implements OnClickListener
{
	protected WebView webView;
	protected Button startSecondActivityBtn;
	
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		
		webView = ((WebView)findViewById(R.id.webView));
		startSecondActivityBtn = ((Button)findViewById(R.id.startSecondActivityBtn));

		// Set the listener
		startSecondActivityBtn.setOnClickListener(this);
		
		// Initialize the WebView
		webView.getSettings().setSupportZoom(true);
		webView.getSettings().setBuiltInZoomControls(true);
		webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);
		webView.setScrollbarFadingEnabled(true);
		webView.getSettings().setLoadsImagesAutomatically(true);

		// Load the URLs inside the WebView, not in the external web browser
		webView.setWebViewClient(new WebViewClient());

		webView.loadUrl("http://www.google.com");
	}

	@Override
	protected void onDestroy()
	{
		// Clear the cache (this clears the WebViews cache for the entire application)
		webView.clearCache(true);
		
		super.onDestroy();
	}
	
	@Override
	public File getCacheDir()
	{
		// NOTE: this method is used in Android 2.1
		
		return getApplicationContext().getCacheDir();
	}

	@Override
	public void onClick(View v)
	{
		if (v == startSecondActivityBtn)
		{
			Intent intent = new Intent(this, SecondActivity.class);
			startActivity(intent);
		}
	}
}

As you can see, the getCacheDir method inside the activity simply calls the same method in the Application context, that means that the ApplicationExt method is called. We’ll have to do this for every activity in the app to make sure the cache path is redirected correctly in Android 2.1, but actually in my tests I found that it looks like the cache manager calls the getCacheDir method inside the first activity that initializes a WebView (if MainActivity doesn’t use a WebView, but only SecondActivity does, then the getCacheDir method of SecondActivity will be called by the cache manager and not the one of MainActivity), anyway I think it’s a good idea to make sure the cache directory is consistent across all the activities of the application.

In MainActivity there’s a button that starts SecondActivity. This is just another activity with a WebView that I used to test the calls to the getCacheDir method in Android 2.1 and see what happens if there’s a WebView also in MainActivity or not. This is the layout of SecondActivity:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:orientation="vertical">

	<WebView android:id="@+id/webView"
		android:layout_width="fill_parent"
		android:layout_height="fill_parent"/>

</LinearLayout>

And this is its source code:

package com.devahead.androidwebviewcacheonsd;

import java.io.File;

import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class SecondActivity extends Activity
{
	protected WebView webView;
	
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.second);
		
		webView = ((WebView)findViewById(R.id.webView));
		
		// Initialize the WebView
		webView.getSettings().setSupportZoom(true);
		webView.getSettings().setBuiltInZoomControls(true);
		webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);
		webView.setScrollbarFadingEnabled(true);
		webView.getSettings().setLoadsImagesAutomatically(true);

		// Load the URLs inside the WebView, not in the external web browser
		webView.setWebViewClient(new WebViewClient());

		webView.loadUrl("http://www.google.com");
	}

	@Override
	public File getCacheDir()
	{
		// NOTE: this method is used in Android 2.1
		
		return getApplicationContext().getCacheDir();
	}
}

It looks similar to the one of MainActivity and we override the getCacheDir method here as well. The main difference is that in MainActivity the cache of the WebViews is cleared inside the onDestroy method. Remember that whenever you call the clearCache method you’re actually clearing the cache for all the WebViews of your application, not only the one that you used to invoke the method, so you must implement your own logic to clear the cache depending on the structure of your app.

This is all you have to do to write the cache of an Android WebView to the external memory. Remember that you’ll probably have to manage also the case when the external storage is unmounted during your app extecution by detecting the event and maybe giving a warning to the user. I hope you found this post useful and in case you’ve got more suggestions about this topic, feel free to write a comment.

Comments

Hakem Zaied
Reply

Your a live saver, thank you very much, really helped :)

NemoDo
Reply

I download your source code and run it. If Internet is connected -> webview show ok, but i disconnect Internet -> Webpage not avalable in all two screen.

NemoDo
Reply

I have resolved my problem, in your code need add these following:

webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
webView.getSettings().setAppCacheMaxSize(1024 * 1024 * 8);

edward
Reply

i used this code in my logcat shows : external/chromium/net/disk_cache/backend_impl.cc:1107: [0730/141615:ERROR:backend_impl.cc(1107)] Critical error found -8 .. may i know whats the problem??.. and in /data/data/ there is no folder named com.devahead.androidwebviewcacheonsd

Stan
Reply

Is it possible to change the caching dir on the fly while app is executing? Seems like this getCacheDir() method gets called only once at an app start…

matthew
Reply

Hi, I have the same problem as NemoDo. His solution does not solve the problem. When the network adapter is on the website displays, whereas when the adapter is off the app will not display the cached website. Can anyone please tell me why?
Thanks in advance

Matt

Craig
Reply

Thanks, this is absolutely brilliant, but I am using it for bulk caching of multiple pages, and it’s not working on Android 4.2. Pages actually viewed on-screen are cached, and will then reload offline, but those saved in a bulk process don’t. I have a logcat message in place to report any errors, and none are showing up. Any ideas?

irving
Reply

it’s not creating the cache path this is wrong

Sargius
Reply

Hi, I am trying to use you code but I have the same error as Mattew and NemoDo. When the network is on the website displays, if it is off the app will not display the cached websie. Could you kindly uplod here or send me a example code which is really working?

Sargis

Hitesh Patel
Reply

Offline no any page load,
I had try in android 2.3 and android 4.2
@NemoDo
I had try it.
webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
webView.getSettings().setAppCacheMaxSize(1024 * 1024 * 8);

but it not worked for me, your suggestion will be appreciated

TaZ
Reply

Thanks for the tutorial, it’s great. But as I’m trying it out in my Nexus 5, I don’t know whether it’s working or not. As expected, it isn’t caching in my phone

to
Reply

Hello!

Leave a comment

name*

email* (not published)

website