Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 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; |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 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 |
Jesse Wilson | 98e8b19 | 2011-06-23 15:59:32 -0700 | [diff] [blame] | 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—even if available—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. |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 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> |
Jesse Wilson | 8543b54 | 2011-12-15 12:25:55 -0500 | [diff] [blame] | 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> |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 149 | */ |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 150 | public final class HttpResponseCache extends ResponseCache implements Closeable { |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 151 | |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 152 | private final com.android.okhttp.HttpResponseCache delegate; |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 153 | |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 154 | private HttpResponseCache(com.android.okhttp.HttpResponseCache delegate) { |
| 155 | this.delegate = delegate; |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 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 | */ |
| 162 | public static HttpResponseCache getInstalled() { |
| 163 | ResponseCache installed = ResponseCache.getDefault(); |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 164 | if (installed instanceof com.android.okhttp.HttpResponseCache) { |
| 165 | return new HttpResponseCache( |
| 166 | (com.android.okhttp.HttpResponseCache) installed); |
| 167 | } |
| 168 | |
| 169 | return null; |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 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 | */ |
| 183 | public static HttpResponseCache install(File directory, long maxSize) throws IOException { |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 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; |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 188 | // don't close and reopen if an equivalent cache is already installed |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 189 | if (installedCache.getDirectory().equals(directory) |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 190 | && installedCache.getMaxSize() == maxSize |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 191 | && !installedCache.isClosed()) { |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 192 | return new HttpResponseCache(installedCache); |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 193 | } else { |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 194 | // The HttpResponseCache that owns this object is about to be replaced. |
| 195 | installedCache.close(); |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 196 | } |
| 197 | } |
| 198 | |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 199 | com.android.okhttp.HttpResponseCache responseCache = |
| 200 | new com.android.okhttp.HttpResponseCache(directory, maxSize); |
| 201 | ResponseCache.setDefault(responseCache); |
| 202 | return new HttpResponseCache(responseCache); |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 203 | } |
| 204 | |
| 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 | |
| 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 | */ |
| 219 | public long size() { |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 220 | return delegate.getSize(); |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 221 | } |
| 222 | |
| 223 | /** |
| 224 | * Returns the maximum number of bytes that this cache should use to store |
| 225 | * its data. |
| 226 | */ |
| 227 | public long maxSize() { |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 228 | return delegate.getMaxSize(); |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 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 | */ |
| 236 | public void flush() { |
| 237 | try { |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 238 | delegate.flush(); |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 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 | */ |
| 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 | */ |
| 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 | */ |
| 265 | public int getRequestCount() { |
| 266 | return delegate.getRequestCount(); |
| 267 | } |
| 268 | |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 269 | /** |
| 270 | * Uninstalls the cache and releases any active resources. Stored contents |
| 271 | * will remain on the filesystem. |
| 272 | */ |
| 273 | @Override public void close() throws IOException { |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 274 | if (ResponseCache.getDefault() == this.delegate) { |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 275 | ResponseCache.setDefault(null); |
| 276 | } |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 277 | delegate.close(); |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 278 | } |
| 279 | |
| 280 | /** |
| 281 | * Uninstalls the cache and deletes all of its stored contents. |
| 282 | */ |
| 283 | public void delete() throws IOException { |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 284 | if (ResponseCache.getDefault() == this.delegate) { |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 285 | ResponseCache.setDefault(null); |
| 286 | } |
Narayan Kamath | 68a3cd7 | 2013-06-06 12:16:40 +0100 | [diff] [blame] | 287 | delegate.delete(); |
Brian Carlstrom | a7284f0 | 2011-05-27 00:07:42 -0700 | [diff] [blame] | 288 | } |
| 289 | } |