blob: 2a5cbe99e15c0c60ab51b29e80a46ba46182cf86 [file] [log] [blame]
// Copyright 2008, The Android Open Source Project
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// 3. Neither the name of Google Inc. nor the names of its contributors may be
// used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package android.webkit.gears;
import android.net.http.Headers;
import android.util.Log;
import android.webkit.CacheManager;
import android.webkit.CacheManager.CacheResult;
import android.webkit.Plugin;
import android.webkit.UrlInterceptRegistry;
import android.webkit.UrlInterceptHandler;
import android.webkit.WebView;
import org.apache.http.impl.cookie.DateUtils;
import org.apache.http.util.CharArrayBuffer;
import java.io.*;
import java.util.*;
/**
* Services requests to handle URLs coming from the browser or
* HttpRequestAndroid. This registers itself with the
* UrlInterceptRegister in Android so we get a chance to service all
* URLs passing through the browser before anything else.
*/
public class UrlInterceptHandlerGears implements UrlInterceptHandler {
/** Singleton instance. */
private static UrlInterceptHandlerGears instance;
/** Debug logging tag. */
private static final String LOG_TAG = "Gears-J";
/** Buffer size for reading/writing streams. */
private static final int BUFFER_SIZE = 4096;
/**
* Number of milliseconds to expire LocalServer temporary entries in
* the browser's cache. Somewhat arbitrarily chosen as a compromise
* between being a) long enough not to expire during page load and
* b) short enough to evict entries during a session. */
private static final int CACHE_EXPIRY_MS = 60000; // 1 minute.
/** Enable/disable all logging in this class. */
private static boolean logEnabled = false;
/** The unmodified (case-sensitive) key in the headers map is the
* same index as used by HttpRequestAndroid. */
public static final int HEADERS_MAP_INDEX_KEY =
ApacheHttpRequestAndroid.HEADERS_MAP_INDEX_KEY;
/** The associated value in the headers map is the same index as
* used by HttpRequestAndroid. */
public static final int HEADERS_MAP_INDEX_VALUE =
ApacheHttpRequestAndroid.HEADERS_MAP_INDEX_VALUE;
/**
* Object passed to the native side, containing information about
* the URL to service.
*/
public static class ServiceRequest {
// The URL being requested.
private String url;
// Request headers. Map of lowercase key to [ unmodified key, value ].
private Map<String, String[]> requestHeaders;
/**
* Initialize members on construction.
* @param url The URL being requested.
* @param requestHeaders Headers associated with the request,
* or null if none.
* Map of lowercase key to [ unmodified key, value ].
*/
public ServiceRequest(String url, Map<String, String[]> requestHeaders) {
this.url = url;
this.requestHeaders = requestHeaders;
}
/**
* Returns the URL being requested.
* @return The URL being requested.
*/
public String getUrl() {
return url;
}
/**
* Get the value associated with a request header key, if any.
* @param header The key to find, case insensitive.
* @return The value associated with this header, or null if not found.
*/
public String getRequestHeader(String header) {
if (requestHeaders != null) {
String[] value = requestHeaders.get(header.toLowerCase());
if (value != null) {
return value[HEADERS_MAP_INDEX_VALUE];
} else {
return null;
}
} else {
return null;
}
}
}
/**
* Object returned by the native side, containing information needed
* to pass the entire response back to the browser or
* HttpRequestAndroid. Works from either an in-memory array or a
* file on disk.
*/
public class ServiceResponse {
// The response status code, e.g 200.
private int statusCode;
// The full status line, e.g "HTTP/1.1 200 OK".
private String statusLine;
// All headers associated with the response. Map of lowercase key
// to [ unmodified key, value ].
private Map<String, String[]> responseHeaders =
new HashMap<String, String[]>();
// The MIME type, e.g "text/html".
private String mimeType;
// The encoding, e.g "utf-8", or null if none.
private String encoding;
// The stream which contains the body when read().
private InputStream inputStream;
/**
* Initialize members using an in-memory array to return the body.
* @param statusCode The response status code, e.g 200.
* @param statusLine The full status line, e.g "HTTP/1.1 200 OK".
* @param mimeType The MIME type, e.g "text/html".
* @param encoding Encoding, e.g "utf-8" or null if none.
* @param body The response body as a byte array, non-empty.
*/
void setResultArray(
int statusCode,
String statusLine,
String mimeType,
String encoding,
byte[] body) {
this.statusCode = statusCode;
this.statusLine = statusLine;
this.mimeType = mimeType;
this.encoding = encoding;
// Setup a stream to read out of the byte array.
this.inputStream = new ByteArrayInputStream(body);
}
/**
* Initialize members using a file on disk to return the body.
* @param statusCode The response status code, e.g 200.
* @param statusLine The full status line, e.g "HTTP/1.1 200 OK".
* @param mimeType The MIME type, e.g "text/html".
* @param encoding Encoding, e.g "utf-8" or null if none.
* @param path Full path to the file containing the body.
* @return True if the file is successfully setup to stream,
* false on error such as file not found.
*/
boolean setResultFile(
int statusCode,
String statusLine,
String mimeType,
String encoding,
String path) {
this.statusCode = statusCode;
this.statusLine = statusLine;
this.mimeType = mimeType;
this.encoding = encoding;
try {
// Setup a stream to read out of a file on disk.
this.inputStream = new FileInputStream(new File(path));
return true;
} catch (java.io.FileNotFoundException ex) {
log("File not found: " + path);
return false;
}
}
/**
* Set a response header, adding its settings to the header members.
* @param key The case sensitive key for the response header,
* e.g "Set-Cookie".
* @param value The value associated with this key, e.g "cookie1234".
*/
public void setResponseHeader(String key, String value) {
// The map value contains the unmodified key (not lowercase).
String[] mapValue = { key, value };
responseHeaders.put(key.toLowerCase(), mapValue);
}
/**
* Return the "Content-Type" header possibly supplied by a
* previous setResponseHeader().
* @return The "Content-Type" value, or null if not present.
*/
public String getContentType() {
// The map keys are lowercase.
String[] value = responseHeaders.get("content-type");
if (value != null) {
return value[HEADERS_MAP_INDEX_VALUE];
} else {
return null;
}
}
/**
* Returns the HTTP status code for the response, supplied in
* setResultArray() or setResultFile().
* @return The HTTP statue code, e.g 200.
*/
public int getStatusCode() {
return statusCode;
}
/**
* Returns the full HTTP status line for the response, supplied in
* setResultArray() or setResultFile().
* @return The HTTP statue line, e.g "HTTP/1.1 200 OK".
*/
public String getStatusLine() {
return statusLine;
}
/**
* Get all response headers supplied in calls in
* setResponseHeader().
* @return A Map<String, String[]> containing all headers.
*/
public Map<String, String[]> getResponseHeaders() {
return responseHeaders;
}
/**
* Returns the MIME type for the response, supplied in
* setResultArray() or setResultFile().
* @return The MIME type, e.g "text/html".
*/
public String getMimeType() {
return mimeType;
}
/**
* Returns the encoding for the response, supplied in
* setResultArray() or setResultFile(), or null if none.
* @return The encoding, e.g "utf-8", or null if none.
*/
public String getEncoding() {
return encoding;
}
/**
* Returns the InputStream setup by setResultArray() or
* setResultFile() to allow reading data either from memory or
* disk.
* @return The InputStream containing the response body.
*/
public InputStream getInputStream() {
return inputStream;
}
}
/**
* Construct and initialize the singleton instance.
*/
public UrlInterceptHandlerGears() {
if (instance != null) {
Log.e(LOG_TAG, "UrlInterceptHandlerGears singleton already constructed");
throw new RuntimeException();
}
instance = this;
}
/**
* Turn on/off logging in this class.
* @param on Logging enable state.
*/
public static void enableLogging(boolean on) {
logEnabled = on;
}
/**
* Get the singleton instance.
* @return The singleton instance.
*/
public static UrlInterceptHandlerGears getInstance() {
return instance;
}
/**
* Register the singleton instance with the browser's interception
* mechanism.
*/
public synchronized void register() {
UrlInterceptRegistry.registerHandler(this);
}
/**
* Unregister the singleton instance from the browser's interception
* mechanism.
*/
public synchronized void unregister() {
UrlInterceptRegistry.unregisterHandler(this);
}
/**
* Copy the entire InputStream to OutputStream.
* @param inputStream The stream to read from.
* @param outputStream The stream to write to.
* @return True if the entire stream copied successfully, false on error.
*/
private boolean copyStream(InputStream inputStream,
OutputStream outputStream) {
try {
// Temporary buffer to copy stream through.
byte[] buf = new byte[BUFFER_SIZE];
for (;;) {
// Read up to BUFFER_SIZE bytes.
int bytes = inputStream.read(buf);
if (bytes < 0) {
break;
}
// Write the number of bytes we just read.
outputStream.write(buf, 0, bytes);
}
} catch (IOException ex) {
log("Got IOException copying stream: " + ex);
return false;
}
return true;
}
/**
* Given an URL, returns a CacheResult which contains the response
* for the request. This implements the UrlInterceptHandler interface.
*
* @param url The fully qualified URL being requested.
* @param requestHeaders The request headers for this URL.
* @return If a response can be crafted, a CacheResult initialized
* to return the surrogate response. If this URL cannot
* be serviced, returns null.
*/
public CacheResult service(String url, Map<String, String> requestHeaders) {
// Thankfully the browser does call us with case-sensitive
// headers. We just need to map it case-insensitive.
Map<String, String[]> lowercaseRequestHeaders =
new HashMap<String, String[]>();
Iterator<Map.Entry<String, String>> requestHeadersIt =
requestHeaders.entrySet().iterator();
while (requestHeadersIt.hasNext()) {
Map.Entry<String, String> entry = requestHeadersIt.next();
String key = entry.getKey();
String mapValue[] = { key, entry.getValue() };
lowercaseRequestHeaders.put(key.toLowerCase(), mapValue);
}
ServiceResponse response = getServiceResponse(url, lowercaseRequestHeaders);
if (response == null) {
// No result for this URL.
return null;
}
// Translate the ServiceResponse to a CacheResult.
// Translate http -> gears, https -> gearss, so we don't overwrite
// existing entries.
String gearsUrl = "gears" + url.substring("http".length());
// Set the result to expire, so that entries don't pollute the
// browser's cache for too long.
long now_ms = System.currentTimeMillis();
String expires = DateUtils.formatDate(new Date(now_ms + CACHE_EXPIRY_MS));
response.setResponseHeader(ApacheHttpRequestAndroid.KEY_EXPIRES, expires);
// The browser is only interested in a small subset of headers,
// contained in a Headers object. Iterate the map of all headers
// and add them to Headers.
Headers headers = new Headers();
Iterator<Map.Entry<String, String[]>> responseHeadersIt =
response.getResponseHeaders().entrySet().iterator();
while (responseHeadersIt.hasNext()) {
Map.Entry<String, String[]> entry = responseHeadersIt.next();
// Headers.parseHeader() expects lowercase keys.
String keyValue = entry.getKey() + ": "
+ entry.getValue()[HEADERS_MAP_INDEX_VALUE];
CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length());
buffer.append(keyValue);
// Parse it into the header container.
headers.parseHeader(buffer);
}
CacheResult cacheResult = CacheManager.createCacheFile(
gearsUrl,
response.getStatusCode(),
headers,
response.getMimeType(),
true); // forceCache
if (cacheResult == null) {
// With the no-cache policy we could end up
// with a null result
return null;
}
// Set encoding if specified.
String encoding = response.getEncoding();
if (encoding != null) {
cacheResult.setEncoding(encoding);
}
// Copy the response body to the CacheResult. This handles all
// combinations of memory vs on-disk on both sides.
InputStream inputStream = response.getInputStream();
OutputStream outputStream = cacheResult.getOutputStream();
boolean copied = copyStream(inputStream, outputStream);
// Close the input and output streams to relinquish their
// resources earlier.
try {
inputStream.close();
} catch (IOException ex) {
log("IOException closing InputStream: " + ex);
copied = false;
}
try {
outputStream.close();
} catch (IOException ex) {
log("IOException closing OutputStream: " + ex);
copied = false;
}
if (!copied) {
log("copyStream of local result failed");
return null;
}
// Save the entry into the browser's cache.
CacheManager.saveCacheFile(gearsUrl, cacheResult);
// Get it back from the cache, this time properly initialized to
// be used for input.
cacheResult = CacheManager.getCacheFile(gearsUrl, null);
if (cacheResult != null) {
log("Returning surrogate result");
return cacheResult;
} else {
// Not an expected condition, but handle gracefully. Perhaps out
// of memory or disk?
Log.e(LOG_TAG, "Lost CacheResult between save and get. Can't serve.\n");
return null;
}
}
/**
* Given an URL, returns a CacheResult and headers which contain the
* response for the request.
*
* @param url The fully qualified URL being requested.
* @param requestHeaders The request headers for this URL.
* @return If a response can be crafted, a ServiceResponse is
* created which contains all response headers and an InputStream
* attached to the body. If there is no response, null is returned.
*/
public ServiceResponse getServiceResponse(String url,
Map<String, String[]> requestHeaders) {
if (!url.startsWith("http://") && !url.startsWith("https://")) {
// Don't know how to service non-HTTP URLs
return null;
}
// Call the native handler to craft a response for this URL.
return nativeService(new ServiceRequest(url, requestHeaders));
}
/**
* Convenience debug function. Calls the Android logging
* mechanism. logEnabled is not a constant, so if the string
* evaluation is potentially expensive, the caller also needs to
* check it.
* @param str String to log to the Android console.
*/
private void log(String str) {
if (logEnabled) {
Log.i(LOG_TAG, str);
}
}
/**
* Native method which handles the bulk of the request in LocalServer.
* @param request A ServiceRequest object containing information about
* the request.
* @return If serviced, a ServiceResponse object containing all the
* information to provide a response for the URL, or null
* if no response available for this URL.
*/
private native static ServiceResponse nativeService(ServiceRequest request);
}