// 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.util.Log;
import android.webkit.CacheManager.CacheResult;
import android.webkit.Plugin;
import android.webkit.PluginData;
import android.webkit.UrlInterceptRegistry;
import android.webkit.UrlInterceptHandler;
import android.webkit.WebView;
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;
/** 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;
// The length of the content body.
private long contentLength;
/**
* 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.contentLength = body.length;
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.
File file = new File(path);
this.contentLength = file.length();
this.inputStream = new FileInputStream(file);
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;
}
/**
* @return The length of the response body.
*/
public long getContentLength() {
return contentLength;
}
}
/**
* 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);
}
/**
* Given an URL, returns the CacheResult which contains the
* surrogate response for the request, or null if the handler is
* not interested.
*
* @param url URL string.
* @param headers The headers associated with the request. May be null.
* @return The CacheResult containing the surrogate response.
* @Deprecated Use PluginData getPluginData(String url,
* Map<String, String> headers); instead
*/
@Deprecated
public CacheResult service(String url, Map<String, String> headers) {
throw new UnsupportedOperationException("unimplemented");
}
/**
* Given an URL, returns a PluginData instance 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 a PluginData object.
*/
public PluginData getPluginData(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;
}
return new PluginData(response.getInputStream(),
response.getContentLength(),
response.getResponseHeaders(),
response.getStatusCode());
}
/**
* 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);
}