1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.http;
18 
19 import android.content.Context;
20 import java.io.Closeable;
21 import java.io.File;
22 import java.io.IOException;
23 import java.net.CacheRequest;
24 import java.net.CacheResponse;
25 import java.net.HttpURLConnection;
26 import java.net.ResponseCache;
27 import java.net.URI;
28 import java.net.URLConnection;
29 import java.util.List;
30 import java.util.Map;
31 import javax.net.ssl.HttpsURLConnection;
32 import org.apache.http.impl.client.DefaultHttpClient;
33 
34 /**
35  * Caches HTTP and HTTPS responses to the filesystem so they may be reused,
36  * saving time and bandwidth. This class supports {@link HttpURLConnection} and
37  * {@link HttpsURLConnection}; there is no platform-provided cache for {@link
38  * DefaultHttpClient} or {@link AndroidHttpClient}.
39  *
40  * <h3>Installing an HTTP response cache</h3>
41  * Enable caching of all of your application's HTTP requests by installing the
42  * cache at application startup. For example, this code installs a 10 MiB cache
43  * in the {@link Context#getCacheDir() application-specific cache directory} of
44  * the filesystem}: <pre>   {@code
45  *   protected void onCreate(Bundle savedInstanceState) {
46  *       ...
47  *
48  *       try {
49  *           File httpCacheDir = new File(context.getCacheDir(), "http");
50  *           long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
51  *           HttpResponseCache.install(httpCacheDir, httpCacheSize);
52  *       } catch (IOException e) {
53  *           Log.i(TAG, "HTTP response cache installation failed:" + e);
54  *       }
55  *   }
56  *
57  *   protected void onStop() {
58  *       ...
59  *
60  *       HttpResponseCache cache = HttpResponseCache.getInstalled();
61  *       if (cache != null) {
62  *           cache.flush();
63  *       }
64  *   }}</pre>
65  * This cache will evict entries as necessary to keep its size from exceeding
66  * 10 MiB. The best cache size is application specific and depends on the size
67  * and frequency of the files being downloaded. Increasing the limit may improve
68  * the hit rate, but it may also just waste filesystem space!
69  *
70  * <p>For some applications it may be preferable to create the cache in the
71  * external storage directory. <strong>There are no access controls on the
72  * external storage directory so it should not be used for caches that could
73  * contain private data.</strong> Although it often has more free space,
74  * external storage is optional and&#8212;even if available&#8212;can disappear
75  * during use. Retrieve the external cache directory using {@link
76  * Context#getExternalCacheDir()}. If this method returns null, your application
77  * should fall back to either not caching or caching on non-external storage. If
78  * the external storage is removed during use, the cache hit rate will drop to
79  * zero and ongoing cache reads will fail.
80  *
81  * <p>Flushing the cache forces its data to the filesystem. This ensures that
82  * all responses written to the cache will be readable the next time the
83  * activity starts.
84  *
85  * <h3>Cache Optimization</h3>
86  * To measure cache effectiveness, this class tracks three statistics:
87  * <ul>
88  *     <li><strong>{@link #getRequestCount() Request Count:}</strong> the number
89  *         of HTTP requests issued since this cache was created.
90  *     <li><strong>{@link #getNetworkCount() Network Count:}</strong> the
91  *         number of those requests that required network use.
92  *     <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of
93  *         those requests whose responses were served by the cache.
94  * </ul>
95  * Sometimes a request will result in a conditional cache hit. If the cache
96  * contains a stale copy of the response, the client will issue a conditional
97  * {@code GET}. The server will then send either the updated response if it has
98  * changed, or a short 'not modified' response if the client's copy is still
99  * valid. Such responses increment both the network count and hit count.
100  *
101  * <p>The best way to improve the cache hit rate is by configuring the web
102  * server to return cacheable responses. Although this client honors all <a
103  * href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache
104  * headers, it doesn't cache partial responses.
105  *
106  * <h3>Force a Network Response</h3>
107  * In some situations, such as after a user clicks a 'refresh' button, it may be
108  * necessary to skip the cache, and fetch data directly from the server. To force
109  * a full refresh, add the {@code no-cache} directive: <pre>   {@code
110  *         connection.addRequestProperty("Cache-Control", "no-cache");
111  * }</pre>
112  * If it is only necessary to force a cached response to be validated by the
113  * server, use the more efficient {@code max-age=0} instead: <pre>   {@code
114  *         connection.addRequestProperty("Cache-Control", "max-age=0");
115  * }</pre>
116  *
117  * <h3>Force a Cache Response</h3>
118  * Sometimes you'll want to show resources if they are available immediately,
119  * but not otherwise. This can be used so your application can show
120  * <i>something</i> while waiting for the latest data to be downloaded. To
121  * restrict a request to locally-cached resources, add the {@code
122  * only-if-cached} directive: <pre>   {@code
123  *     try {
124  *         connection.addRequestProperty("Cache-Control", "only-if-cached");
125  *         InputStream cached = connection.getInputStream();
126  *         // the resource was cached! show it
127  *     } catch (FileNotFoundException e) {
128  *         // the resource was not cached
129  *     }
130  * }</pre>
131  * This technique works even better in situations where a stale response is
132  * better than no response. To permit stale cached responses, use the {@code
133  * max-stale} directive with the maximum staleness in seconds: <pre>   {@code
134  *         int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
135  *         connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
136  * }</pre>
137  *
138  * <h3>Working With Earlier Releases</h3>
139  * This class was added in Android 4.0 (Ice Cream Sandwich). Use reflection to
140  * enable the response cache without impacting earlier releases: <pre>   {@code
141  *       try {
142  *           File httpCacheDir = new File(context.getCacheDir(), "http");
143  *           long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
144  *           Class.forName("android.net.http.HttpResponseCache")
145  *                   .getMethod("install", File.class, long.class)
146  *                   .invoke(null, httpCacheDir, httpCacheSize);
147  *       } catch (Exception httpResponseCacheNotAvailable) {
148  *       }}</pre>
149  */
150 public final class HttpResponseCache extends ResponseCache implements Closeable {
151 
152     private final com.android.okhttp.HttpResponseCache delegate;
153 
HttpResponseCache(com.android.okhttp.HttpResponseCache delegate)154     private HttpResponseCache(com.android.okhttp.HttpResponseCache delegate) {
155         this.delegate = delegate;
156     }
157 
158     /**
159      * Returns the currently-installed {@code HttpResponseCache}, or null if
160      * there is no cache installed or it is not a {@code HttpResponseCache}.
161      */
getInstalled()162     public static HttpResponseCache getInstalled() {
163         ResponseCache installed = ResponseCache.getDefault();
164         if (installed instanceof com.android.okhttp.HttpResponseCache) {
165             return new HttpResponseCache(
166                     (com.android.okhttp.HttpResponseCache) installed);
167         }
168 
169         return null;
170     }
171 
172     /**
173      * Creates a new HTTP response cache and {@link ResponseCache#setDefault
174      * sets it} as the system default cache.
175      *
176      * @param directory the directory to hold cache data.
177      * @param maxSize the maximum size of the cache in bytes.
178      * @return the newly-installed cache
179      * @throws IOException if {@code directory} cannot be used for this cache.
180      *     Most applications should respond to this exception by logging a
181      *     warning.
182      */
install(File directory, long maxSize)183     public static HttpResponseCache install(File directory, long maxSize) throws IOException {
184         ResponseCache installed = ResponseCache.getDefault();
185         if (installed instanceof com.android.okhttp.HttpResponseCache) {
186             com.android.okhttp.HttpResponseCache installedCache =
187                     (com.android.okhttp.HttpResponseCache) installed;
188             // don't close and reopen if an equivalent cache is already installed
189             if (installedCache.getDirectory().equals(directory)
190                     && installedCache.getMaxSize() == maxSize
191                     && !installedCache.isClosed()) {
192                 return new HttpResponseCache(installedCache);
193             } else {
194                 // The HttpResponseCache that owns this object is about to be replaced.
195                 installedCache.close();
196             }
197         }
198 
199         com.android.okhttp.HttpResponseCache responseCache =
200                 new com.android.okhttp.HttpResponseCache(directory, maxSize);
201         ResponseCache.setDefault(responseCache);
202         return new HttpResponseCache(responseCache);
203     }
204 
get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)205     @Override public CacheResponse get(URI uri, String requestMethod,
206             Map<String, List<String>> requestHeaders) throws IOException {
207         return delegate.get(uri, requestMethod, requestHeaders);
208     }
209 
put(URI uri, URLConnection urlConnection)210     @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
211         return delegate.put(uri, urlConnection);
212     }
213 
214     /**
215      * Returns the number of bytes currently being used to store the values in
216      * this cache. This may be greater than the {@link #maxSize} if a background
217      * deletion is pending.
218      */
size()219     public long size() {
220         return delegate.getSize();
221     }
222 
223     /**
224      * Returns the maximum number of bytes that this cache should use to store
225      * its data.
226      */
maxSize()227     public long maxSize() {
228         return delegate.getMaxSize();
229     }
230 
231     /**
232      * Force buffered operations to the filesystem. This ensures that responses
233      * written to the cache will be available the next time the cache is opened,
234      * even if this process is killed.
235      */
flush()236     public void flush() {
237         try {
238             delegate.flush();
239         } catch (IOException ignored) {
240         }
241     }
242 
243     /**
244      * Returns the number of HTTP requests that required the network to either
245      * supply a response or validate a locally cached response.
246      */
getNetworkCount()247     public int getNetworkCount() {
248         return delegate.getNetworkCount();
249     }
250 
251     /**
252      * Returns the number of HTTP requests whose response was provided by the
253      * cache. This may include conditional {@code GET} requests that were
254      * validated over the network.
255      */
getHitCount()256     public int getHitCount() {
257         return delegate.getHitCount();
258     }
259 
260     /**
261      * Returns the total number of HTTP requests that were made. This includes
262      * both client requests and requests that were made on the client's behalf
263      * to handle a redirects and retries.
264      */
getRequestCount()265     public int getRequestCount() {
266         return delegate.getRequestCount();
267     }
268 
269     /**
270      * Uninstalls the cache and releases any active resources. Stored contents
271      * will remain on the filesystem.
272      */
close()273     @Override public void close() throws IOException {
274         if (ResponseCache.getDefault() == this.delegate) {
275             ResponseCache.setDefault(null);
276         }
277         delegate.close();
278     }
279 
280     /**
281      * Uninstalls the cache and deletes all of its stored contents.
282      */
delete()283     public void delete() throws IOException {
284         if (ResponseCache.getDefault() == this.delegate) {
285             ResponseCache.setDefault(null);
286         }
287         delegate.delete();
288     }
289 }
290