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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<?xmlversion="1.0"encoding="utf-8"?>
<manifestxmlns:android="http://schemas.android.com/apk/res/android"
package="com.devahead.androidwebviewcacheonsd"
android:versionCode="1"
android:versionName="1.0">
<uses-sdkandroid:minSdkVersion="7"/>
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permissionandroid:name="android.permission.INTERNET"/>
<uses-permissionandroid: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>
<actionandroid:name="android.intent.action.MAIN"/>
<categoryandroid: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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
packagecom.devahead.androidwebviewcacheonsd;
importjava.io.File;
importandroid.app.Application;
importandroid.os.Environment;
publicclassApplicationExtextendsApplication
{
// NOTE: the content of this path will be deleted
// when the application is uninstalled (Android 2.2 and higher)
protectedFile extStorageAppBasePath;
protectedFile extStorageAppCachePath;
@Override
publicvoidonCreate()
{
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 =newFile(externalStorageDir.getAbsolutePath() +
File.separator +"Android"+ File.separator +"data"+
File.separator + getPackageName());
}
if(extStorageAppBasePath !=null)
{
// {SD_PATH}/Android/data/com.devahead.androidwebviewcacheonsd/cache
extStorageAppCachePath =newFile(extStorageAppBasePath.getAbsolutePath() +
File.separator +"cache");
booleanisCachePathAvailable =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
publicFile getCacheDir()
{
// NOTE: this method is used in Android 2.2 and higher
if(extStorageAppCachePath !=null)
{
// Use the external storage for the cache
returnextStorageAppCachePath;
}
else
{
// /data/data/com.devahead.androidwebviewcacheonsd/cache
returnsuper.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 becausecom.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 Applicationcontext, 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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
<?xmlversion="1.0"encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<Buttonandroid:id="@+id/startSecondActivityBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Start second activity"/>
<WebViewandroid: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 forMainActivity looks like this:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
packagecom.devahead.androidwebviewcacheonsd;
importjava.io.File;
importandroid.app.Activity;
importandroid.content.Intent;
importandroid.os.Bundle;
importandroid.view.View;
importandroid.view.View.OnClickListener;
importandroid.webkit.WebView;
importandroid.webkit.WebViewClient;
importandroid.widget.Button;
publicclassMainActivityextendsActivityimplementsOnClickListener
{
protectedWebView webView;
protectedButton startSecondActivityBtn;
@Override
publicvoidonCreate(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(newWebViewClient());
webView.loadUrl("http://www.google.com");
}
@Override
protectedvoidonDestroy()
{
// Clear the cache (this clears the WebViews cache for the entire application)
webView.clearCache(true);
super.onDestroy();
}
@Override
publicFile getCacheDir()
{
// NOTE: this method is used in Android 2.1
returngetApplicationContext().getCacheDir();
}
@Override
publicvoidonClick(View v)
{
if(v == startSecondActivityBtn)
{
Intent intent =newIntent(this, SecondActivity.class);
startActivity(intent);
}
}
}
|
As you can see, the getCacheDir method inside the activity simply calls the same method in theApplication 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 onlySecondActivity 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 aWebView 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:
01
02
03
04
05
06
07
08
09
10
11
|
<?xmlversion="1.0"encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<WebViewandroid:id="@+id/webView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</LinearLayout>
|
And this is its source code:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
packagecom.devahead.androidwebviewcacheonsd;
importjava.io.File;
importandroid.app.Activity;
importandroid.os.Bundle;
importandroid.webkit.WebView;
importandroid.webkit.WebViewClient;
publicclassSecondActivityextendsActivity
{
protectedWebView webView;
@Override
publicvoidonCreate(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(newWebViewClient());
webView.loadUrl("http://www.google.com");
}
@Override
publicFile getCacheDir()
{
// NOTE: this method is used in Android 2.1
returngetApplicationContext().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 theonDestroy 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.