summaryrefslogtreecommitdiffstats
path: root/core/java/android/webkit/gears
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
commit9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch)
treed88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/webkit/gears
parentd83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff)
downloadframeworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/webkit/gears')
-rw-r--r--core/java/android/webkit/gears/AndroidGpsLocationProvider.java156
-rw-r--r--core/java/android/webkit/gears/AndroidRadioDataProvider.java244
-rw-r--r--core/java/android/webkit/gears/AndroidWifiDataProvider.java136
-rw-r--r--core/java/android/webkit/gears/ApacheHttpRequestAndroid.java1122
-rw-r--r--core/java/android/webkit/gears/DesktopAndroid.java109
-rw-r--r--core/java/android/webkit/gears/NativeDialog.java142
-rw-r--r--core/java/android/webkit/gears/PluginSettings.java79
-rw-r--r--core/java/android/webkit/gears/UrlInterceptHandlerGears.java501
-rw-r--r--core/java/android/webkit/gears/VersionExtractor.java147
-rw-r--r--core/java/android/webkit/gears/ZipInflater.java200
-rw-r--r--core/java/android/webkit/gears/package.html3
11 files changed, 2839 insertions, 0 deletions
diff --git a/core/java/android/webkit/gears/AndroidGpsLocationProvider.java b/core/java/android/webkit/gears/AndroidGpsLocationProvider.java
new file mode 100644
index 0000000..3646042
--- /dev/null
+++ b/core/java/android/webkit/gears/AndroidGpsLocationProvider.java
@@ -0,0 +1,156 @@
+// 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.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationProvider;
+import android.os.Bundle;
+import android.util.Log;
+import android.webkit.WebView;
+
+/**
+ * GPS provider implementation for Android.
+ */
+public final class AndroidGpsLocationProvider implements LocationListener {
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "Gears-J-GpsProvider";
+ /**
+ * Our location manager instance.
+ */
+ private LocationManager locationManager;
+ /**
+ * The native object ID.
+ */
+ private long nativeObject;
+
+ public AndroidGpsLocationProvider(WebView webview, long object) {
+ nativeObject = object;
+ locationManager = (LocationManager) webview.getContext().getSystemService(
+ Context.LOCATION_SERVICE);
+ if (locationManager == null) {
+ Log.e(TAG,
+ "AndroidGpsLocationProvider: could not get location manager.");
+ throw new NullPointerException(
+ "AndroidGpsLocationProvider: locationManager is null.");
+ }
+ // Register for location updates.
+ try {
+ locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0,
+ this);
+ } catch (IllegalArgumentException ex) {
+ Log.e(TAG,
+ "AndroidLocationGpsProvider: could not register for updates: " + ex);
+ throw ex;
+ } catch (SecurityException ex) {
+ Log.e(TAG,
+ "AndroidGpsLocationProvider: not allowed to register for update: "
+ + ex);
+ throw ex;
+ }
+ }
+
+ /**
+ * Called when the provider is no longer needed.
+ */
+ public void shutdown() {
+ locationManager.removeUpdates(this);
+ Log.i(TAG, "GPS provider closed.");
+ }
+
+ /**
+ * Called when the location has changed.
+ * @param location The new location, as a Location object.
+ */
+ public void onLocationChanged(Location location) {
+ Log.i(TAG, "Location changed: " + location);
+ nativeLocationChanged(location, nativeObject);
+ }
+
+ /**
+ * Called when the provider status changes.
+ *
+ * @param provider the name of the location provider associated with this
+ * update.
+ * @param status {@link LocationProvider#OUT_OF_SERVICE} if the
+ * provider is out of service, and this is not expected to change in the
+ * near future; {@link LocationProvider#TEMPORARILY_UNAVAILABLE} if
+ * the provider is temporarily unavailable but is expected to be available
+ * shortly; and {@link LocationProvider#AVAILABLE} if the
+ * provider is currently available.
+ * @param extras an optional Bundle which will contain provider specific
+ * status variables (such as number of satellites).
+ */
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ Log.i(TAG, "Provider " + provider + " status changed to " + status);
+ if (status == LocationProvider.OUT_OF_SERVICE ||
+ status == LocationProvider.TEMPORARILY_UNAVAILABLE) {
+ nativeProviderError(false, nativeObject);
+ }
+ }
+
+ /**
+ * Called when the provider is enabled.
+ *
+ * @param provider the name of the location provider that is now enabled.
+ */
+ public void onProviderEnabled(String provider) {
+ Log.i(TAG, "Provider " + provider + " enabled.");
+ // No need to notify the native side. It's enough to start sending
+ // valid position fixes again.
+ }
+
+ /**
+ * Called when the provider is disabled.
+ *
+ * @param provider the name of the location provider that is now disabled.
+ */
+ public void onProviderDisabled(String provider) {
+ Log.i(TAG, "Provider " + provider + " disabled.");
+ nativeProviderError(true, nativeObject);
+ }
+
+ /**
+ * The native method called when a new location is available.
+ * @param location is the new Location instance to pass to the native side.
+ * @param nativeObject is a pointer to the corresponding
+ * AndroidGpsLocationProvider C++ instance.
+ */
+ private native void nativeLocationChanged(Location location, long object);
+
+ /**
+ * The native method called when there is a GPS provder error.
+ * @param isDisabled is true when the error signifies the fact that the GPS
+ * HW is disabled. For other errors, this param is always false.
+ * @param nativeObject is a pointer to the corresponding
+ * AndroidGpsLocationProvider C++ instance.
+ */
+ private native void nativeProviderError(boolean isDisabled, long object);
+}
diff --git a/core/java/android/webkit/gears/AndroidRadioDataProvider.java b/core/java/android/webkit/gears/AndroidRadioDataProvider.java
new file mode 100644
index 0000000..c920d45
--- /dev/null
+++ b/core/java/android/webkit/gears/AndroidRadioDataProvider.java
@@ -0,0 +1,244 @@
+// 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.content.Context;
+import android.telephony.CellLocation;
+import android.telephony.ServiceState;
+import android.telephony.gsm.GsmCellLocation;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.webkit.WebView;
+
+/**
+ * Radio data provider implementation for Android.
+ */
+public final class AndroidRadioDataProvider extends PhoneStateListener {
+
+ /** Logging tag */
+ private static final String TAG = "Gears-J-RadioProvider";
+
+ /** Network types */
+ private static final int RADIO_TYPE_UNKNOWN = 0;
+ private static final int RADIO_TYPE_GSM = 1;
+ private static final int RADIO_TYPE_WCDMA = 2;
+
+ /** Simple container for radio data */
+ public static final class RadioData {
+ public int cellId = -1;
+ public int locationAreaCode = -1;
+ public int signalStrength = -1;
+ public int mobileCountryCode = -1;
+ public int mobileNetworkCode = -1;
+ public int homeMobileCountryCode = -1;
+ public int homeMobileNetworkCode = -1;
+ public int radioType = RADIO_TYPE_UNKNOWN;
+ public String carrierName;
+
+ /**
+ * Constructs radioData object from the given telephony data.
+ * @param telephonyManager contains the TelephonyManager instance.
+ * @param cellLocation contains information about the current GSM cell.
+ * @param signalStrength is the strength of the network signal.
+ * @param serviceState contains information about the network service.
+ * @return a new RadioData object populated with the currently
+ * available network information or null if there isn't
+ * enough information.
+ */
+ public static RadioData getInstance(TelephonyManager telephonyManager,
+ CellLocation cellLocation, int signalStrength,
+ ServiceState serviceState) {
+
+ if (!(cellLocation instanceof GsmCellLocation)) {
+ // This also covers the case when cellLocation is null.
+ // When that happens, we do not bother creating a
+ // RadioData instance.
+ return null;
+ }
+
+ RadioData radioData = new RadioData();
+ GsmCellLocation gsmCellLocation = (GsmCellLocation) cellLocation;
+
+ // Extract the cell id, LAC, and signal strength.
+ radioData.cellId = gsmCellLocation.getCid();
+ radioData.locationAreaCode = gsmCellLocation.getLac();
+ radioData.signalStrength = signalStrength;
+
+ // Extract the home MCC and home MNC.
+ String operator = telephonyManager.getSimOperator();
+ radioData.setMobileCodes(operator, true);
+
+ if (serviceState != null) {
+ // Extract the carrier name.
+ radioData.carrierName = serviceState.getOperatorAlphaLong();
+
+ // Extract the MCC and MNC.
+ operator = serviceState.getOperatorNumeric();
+ radioData.setMobileCodes(operator, false);
+ }
+
+ // Finally get the radio type.
+ int type = telephonyManager.getNetworkType();
+ if (type == TelephonyManager.NETWORK_TYPE_UMTS) {
+ radioData.radioType = RADIO_TYPE_WCDMA;
+ } else if (type == TelephonyManager.NETWORK_TYPE_GPRS
+ || type == TelephonyManager.NETWORK_TYPE_EDGE) {
+ radioData.radioType = RADIO_TYPE_GSM;
+ }
+
+ // Print out what we got.
+ Log.i(TAG, "Got the following data:");
+ Log.i(TAG, "CellId: " + radioData.cellId);
+ Log.i(TAG, "LAC: " + radioData.locationAreaCode);
+ Log.i(TAG, "MNC: " + radioData.mobileNetworkCode);
+ Log.i(TAG, "MCC: " + radioData.mobileCountryCode);
+ Log.i(TAG, "home MNC: " + radioData.homeMobileNetworkCode);
+ Log.i(TAG, "home MCC: " + radioData.homeMobileCountryCode);
+ Log.i(TAG, "Signal strength: " + radioData.signalStrength);
+ Log.i(TAG, "Carrier: " + radioData.carrierName);
+ Log.i(TAG, "Network type: " + radioData.radioType);
+
+ return radioData;
+ }
+
+ private RadioData() {}
+
+ /**
+ * Parses a string containing a mobile country code and a mobile
+ * network code and sets the corresponding member variables.
+ * @param codes is the string to parse.
+ * @param homeValues flags whether the codes are for the home operator.
+ */
+ private void setMobileCodes(String codes, boolean homeValues) {
+ if (codes != null) {
+ try {
+ // The operator numeric format is 3 digit country code plus 2 or
+ // 3 digit network code.
+ int mcc = Integer.parseInt(codes.substring(0, 3));
+ int mnc = Integer.parseInt(codes.substring(3));
+ if (homeValues) {
+ homeMobileCountryCode = mcc;
+ homeMobileNetworkCode = mnc;
+ } else {
+ mobileCountryCode = mcc;
+ mobileNetworkCode = mnc;
+ }
+ } catch (IndexOutOfBoundsException ex) {
+ Log.e(
+ TAG,
+ "AndroidRadioDataProvider: Invalid operator numeric data: " + ex);
+ } catch (NumberFormatException ex) {
+ Log.e(
+ TAG,
+ "AndroidRadioDataProvider: Operator numeric format error: " + ex);
+ }
+ }
+ }
+ };
+
+ /** The native object ID */
+ private long nativeObject;
+
+ /** The last known cellLocation */
+ private CellLocation cellLocation = null;
+
+ /** The last known signal strength */
+ private int signalStrength = -1;
+
+ /** The last known serviceState */
+ private ServiceState serviceState = null;
+
+ /**
+ * Our TelephonyManager instance.
+ */
+ private TelephonyManager telephonyManager;
+
+ /**
+ * Public constructor. Uses the webview to get the Context object.
+ */
+ public AndroidRadioDataProvider(WebView webview, long object) {
+ super();
+ nativeObject = object;
+ telephonyManager = (TelephonyManager) webview.getContext().getSystemService(
+ Context.TELEPHONY_SERVICE);
+ if (telephonyManager == null) {
+ Log.e(TAG,
+ "AndroidRadioDataProvider: could not get tepephony manager.");
+ throw new NullPointerException(
+ "AndroidRadioDataProvider: telephonyManager is null.");
+ }
+
+ // Register for cell id, signal strength and service state changed
+ // notifications.
+ telephonyManager.listen(this, PhoneStateListener.LISTEN_CELL_LOCATION
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTH
+ | PhoneStateListener.LISTEN_SERVICE_STATE);
+ }
+
+ /**
+ * Should be called when the provider is no longer needed.
+ */
+ public void shutdown() {
+ telephonyManager.listen(this, PhoneStateListener.LISTEN_NONE);
+ Log.i(TAG, "AndroidRadioDataProvider shutdown.");
+ }
+
+ @Override
+ public void onServiceStateChanged(ServiceState state) {
+ serviceState = state;
+ notifyListeners();
+ }
+
+ @Override
+ public void onSignalStrengthChanged(int asu) {
+ signalStrength = asu;
+ notifyListeners();
+ }
+
+ @Override
+ public void onCellLocationChanged(CellLocation location) {
+ cellLocation = location;
+ notifyListeners();
+ }
+
+ private void notifyListeners() {
+ RadioData radioData = RadioData.getInstance(telephonyManager, cellLocation,
+ signalStrength, serviceState);
+ if (radioData != null) {
+ onUpdateAvailable(radioData, nativeObject);
+ }
+ }
+
+ /**
+ * The native method called when new radio data is available.
+ * @param radioData is the RadioData instance to pass to the native side.
+ * @param nativeObject is a pointer to the corresponding
+ * AndroidRadioDataProvider C++ instance.
+ */
+ private static native void onUpdateAvailable(
+ RadioData radioData, long nativeObject);
+}
diff --git a/core/java/android/webkit/gears/AndroidWifiDataProvider.java b/core/java/android/webkit/gears/AndroidWifiDataProvider.java
new file mode 100644
index 0000000..7379f59
--- /dev/null
+++ b/core/java/android/webkit/gears/AndroidWifiDataProvider.java
@@ -0,0 +1,136 @@
+// Copyright 2008, Google Inc.
+//
+// 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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Config;
+import android.util.Log;
+import android.webkit.WebView;
+import java.util.List;
+
+/**
+ * WiFi data provider implementation for Android.
+ * {@hide}
+ */
+public final class AndroidWifiDataProvider extends BroadcastReceiver {
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "Gears-J-WifiProvider";
+ /**
+ * Our Wifi manager instance.
+ */
+ private WifiManager mWifiManager;
+ /**
+ * The native object ID.
+ */
+ private long mNativeObject;
+ /**
+ * The Context instance.
+ */
+ private Context mContext;
+
+ /**
+ * Constructs a instance of this class and registers for wifi scan
+ * updates. Note that this constructor must be called on a Looper
+ * thread. Suitable threads can be created on the native side using
+ * the AndroidLooperThread C++ class.
+ */
+ public AndroidWifiDataProvider(WebView webview, long object) {
+ mNativeObject = object;
+ mContext = webview.getContext();
+ mWifiManager =
+ (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ if (mWifiManager == null) {
+ Log.e(TAG,
+ "AndroidWifiDataProvider: could not get location manager.");
+ throw new NullPointerException(
+ "AndroidWifiDataProvider: locationManager is null.");
+ }
+
+ // Create a Handler that identifies the message loop associated
+ // with the current thread. Note that it is not necessary to
+ // override handleMessage() at all since the Intent
+ // ReceiverDispatcher (see the ActivityThread class) only uses
+ // this handler to post a Runnable to this thread's loop.
+ Handler handler = new Handler(Looper.myLooper());
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(mWifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+ mContext.registerReceiver(this, filter, null, handler);
+
+ // Get the last scan results and pass them to the native side.
+ // We can't just invoke the callback here, so we queue a message
+ // to this thread's loop.
+ handler.post(new Runnable() {
+ public void run() {
+ onUpdateAvailable(mWifiManager.getScanResults(), mNativeObject);
+ }
+ });
+ }
+
+ /**
+ * Called when the provider is no longer needed.
+ */
+ public void shutdown() {
+ mContext.unregisterReceiver(this);
+ if (Config.LOGV) {
+ Log.v(TAG, "Wifi provider closed.");
+ }
+ }
+
+ /**
+ * This method is called when the AndroidWifiDataProvider is receiving an
+ * Intent broadcast.
+ * @param context The Context in which the receiver is running.
+ * @param intent The Intent being received.
+ */
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(
+ mWifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
+ if (Config.LOGV) {
+ Log.v(TAG, "Wifi scan resulst available");
+ }
+ onUpdateAvailable(mWifiManager.getScanResults(), mNativeObject);
+ }
+ }
+
+ /**
+ * The native method called when new wifi data is available.
+ * @param scanResults is a list of ScanResults to pass to the native side.
+ * @param nativeObject is a pointer to the corresponding
+ * AndroidWifiDataProvider C++ instance.
+ */
+ private static native void onUpdateAvailable(
+ List<ScanResult> scanResults, long nativeObject);
+}
diff --git a/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java b/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java
new file mode 100644
index 0000000..0569255
--- /dev/null
+++ b/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java
@@ -0,0 +1,1122 @@
+// 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.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Config;
+import android.util.Log;
+import android.webkit.CacheManager;
+import android.webkit.CacheManager.CacheResult;
+import android.webkit.CookieManager;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+import java.lang.StringBuilder;
+import java.util.Date;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.HttpResponse;
+import org.apache.http.entity.AbstractHttpEntity;
+import org.apache.http.client.*;
+import org.apache.http.client.methods.*;
+import org.apache.http.impl.client.AbstractHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.util.CharArrayBuffer;
+
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Performs the underlying HTTP/HTTPS GET, POST, HEAD, PUT, DELETE requests.
+ * <p> These are performed synchronously (blocking). The caller should
+ * ensure that it is in a background thread if asynchronous behavior
+ * is required. All data is pushed, so there is no need for JNI native
+ * callbacks.
+ * <p> This uses Apache's HttpClient framework to perform most
+ * of the underlying network activity. The Android brower's cache,
+ * android.webkit.CacheManager, is also used when caching is enabled,
+ * and updated with new data. The android.webkit.CookieManager is also
+ * queried and updated as necessary.
+ * <p> The public interface is designed to be called by native code
+ * through JNI, and to simplify coding none of the public methods will
+ * surface a checked exception. Unchecked exceptions may still be
+ * raised but only if the system is in an ill state, such as out of
+ * memory.
+ * <p> TODO: This isn't plumbed into LocalServer yet. Mutually
+ * dependent on LocalServer - will attach the two together once both
+ * are submitted.
+ */
+public final class ApacheHttpRequestAndroid {
+ /** Debug logging tag. */
+ private static final String LOG_TAG = "Gears-J";
+ /** HTTP response header line endings are CR-LF style. */
+ private static final String HTTP_LINE_ENDING = "\r\n";
+ /** Safe MIME type to use whenever it isn't specified. */
+ private static final String DEFAULT_MIME_TYPE = "text/plain";
+ /** Case-sensitive header keys */
+ public static final String KEY_CONTENT_LENGTH = "Content-Length";
+ public static final String KEY_EXPIRES = "Expires";
+ public static final String KEY_LAST_MODIFIED = "Last-Modified";
+ public static final String KEY_ETAG = "ETag";
+ public static final String KEY_LOCATION = "Location";
+ public static final String KEY_CONTENT_TYPE = "Content-Type";
+ /** Number of bytes to send and receive on the HTTP connection in
+ * one go. */
+ private static final int BUFFER_SIZE = 4096;
+
+ /** The first element of the String[] value in a headers map is the
+ * unmodified (case-sensitive) key. */
+ public static final int HEADERS_MAP_INDEX_KEY = 0;
+ /** The second element of the String[] value in a headers map is the
+ * associated value. */
+ public static final int HEADERS_MAP_INDEX_VALUE = 1;
+
+ /** Request headers, as key -> value map. */
+ // TODO: replace this design by a simpler one (the C++ side has to
+ // be modified too), where we do not store both the original header
+ // and the lowercase one.
+ private Map<String, String[]> mRequestHeaders =
+ new HashMap<String, String[]>();
+ /** Response headers, as a lowercase key -> value map. */
+ private Map<String, String[]> mResponseHeaders =
+ new HashMap<String, String[]>();
+ /** The URL used for createCacheResult() */
+ private String mCacheResultUrl;
+ /** CacheResult being saved into, if inserting a new cache entry. */
+ private CacheResult mCacheResult;
+ /** Initialized by initChildThread(). Used to target abort(). */
+ private Thread mBridgeThread;
+
+ /** Our HttpClient */
+ private AbstractHttpClient mClient;
+ /** The HttpMethod associated with this request */
+ private HttpRequestBase mMethod;
+ /** The complete response line e.g "HTTP/1.0 200 OK" */
+ private String mResponseLine;
+ /** HTTP body stream, setup after connection. */
+ private InputStream mBodyInputStream;
+
+ /** HTTP Response Entity */
+ private HttpResponse mResponse;
+
+ /** Post Entity, used to stream the request to the server */
+ private StreamEntity mPostEntity = null;
+ /** Content lenght, mandatory when using POST */
+ private long mContentLength;
+
+ /** The request executes in a parallel thread */
+ private Thread mHttpThread = null;
+ /** protect mHttpThread, if interrupt() is called concurrently */
+ private Lock mHttpThreadLock = new ReentrantLock();
+ /** Flag set to true when the request thread is joined */
+ private boolean mConnectionFinished = false;
+ /** Flag set to true by interrupt() and/or connection errors */
+ private boolean mConnectionFailed = false;
+ /** Lock protecting the access to mConnectionFailed */
+ private Lock mConnectionFailedLock = new ReentrantLock();
+
+ /** Lock on the loop in StreamEntity */
+ private Lock mStreamingReadyLock = new ReentrantLock();
+ /** Condition variable used to signal the loop is ready... */
+ private Condition mStreamingReady = mStreamingReadyLock.newCondition();
+
+ /** Used to pass around the block of data POSTed */
+ private Buffer mBuffer = new Buffer();
+ /** Used to signal that the block of data has been written */
+ private SignalConsumed mSignal = new SignalConsumed();
+
+ // inner classes
+
+ /**
+ * Implements the http request
+ */
+ class Connection implements Runnable {
+ public void run() {
+ boolean problem = false;
+ try {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "REQUEST : " + mMethod.getRequestLine());
+ }
+ mResponse = mClient.execute(mMethod);
+ if (mResponse != null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "response (status line): "
+ + mResponse.getStatusLine());
+ }
+ mResponseLine = "" + mResponse.getStatusLine();
+ } else {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "problem, response == null");
+ }
+ problem = true;
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Connection IO exception ", e);
+ problem = true;
+ } catch (RuntimeException e) {
+ Log.e(LOG_TAG, "Connection runtime exception ", e);
+ problem = true;
+ }
+
+ if (!problem) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Request complete ("
+ + mMethod.getRequestLine() + ")");
+ }
+ } else {
+ mConnectionFailedLock.lock();
+ mConnectionFailed = true;
+ mConnectionFailedLock.unlock();
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Request FAILED ("
+ + mMethod.getRequestLine() + ")");
+ }
+ // We abort the execution in order to shutdown and release
+ // the underlying connection
+ mMethod.abort();
+ if (mPostEntity != null) {
+ // If there is a post entity, we need to wake it up from
+ // a potential deadlock
+ mPostEntity.signalOutputStream();
+ }
+ }
+ }
+ }
+
+ /**
+ * simple buffer class implementing a producer/consumer model
+ */
+ class Buffer {
+ private DataPacket mPacket;
+ private boolean mEmpty = true;
+ public synchronized void put(DataPacket packet) {
+ while (!mEmpty) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException while putting " +
+ "a DataPacket in the Buffer: " + e);
+ }
+ }
+ }
+ mPacket = packet;
+ mEmpty = false;
+ notify();
+ }
+ public synchronized DataPacket get() {
+ while (mEmpty) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException while getting " +
+ "a DataPacket in the Buffer: " + e);
+ }
+ }
+ }
+ mEmpty = true;
+ notify();
+ return mPacket;
+ }
+ }
+
+ /**
+ * utility class used to block until the packet is signaled as being
+ * consumed
+ */
+ class SignalConsumed {
+ private boolean mConsumed = false;
+ public synchronized void waitUntilPacketConsumed() {
+ while (!mConsumed) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException while waiting " +
+ "until a DataPacket is consumed: " + e);
+ }
+ }
+ }
+ mConsumed = false;
+ notify();
+ }
+ public synchronized void packetConsumed() {
+ while (mConsumed) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException while indicating "
+ + "that the DataPacket has been consumed: " + e);
+ }
+ }
+ }
+ mConsumed = true;
+ notify();
+ }
+ }
+
+ /**
+ * Utility class encapsulating a packet of data
+ */
+ class DataPacket {
+ private byte[] mContent;
+ private int mLength;
+ public DataPacket(byte[] content, int length) {
+ mContent = content;
+ mLength = length;
+ }
+ public byte[] getBytes() {
+ return mContent;
+ }
+ public int getLength() {
+ return mLength;
+ }
+ }
+
+ /**
+ * HttpEntity class to write the bytes received by the C++ thread
+ * on the connection outputstream, in a streaming way.
+ * This entity is executed in the request thread.
+ * The writeTo() method is automatically called by the
+ * HttpPost execution; upon reception, we loop while receiving
+ * the data packets from the main thread, until completion
+ * or error. When done, we flush the outputstream.
+ * The main thread (sendPostData()) also blocks until the
+ * outputstream is made available (or an error happens)
+ */
+ class StreamEntity implements HttpEntity {
+ private OutputStream mOutputStream;
+
+ // HttpEntity interface methods
+
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ public boolean isChunked() {
+ return false;
+ }
+
+ public long getContentLength() {
+ return mContentLength;
+ }
+
+ public Header getContentType() {
+ return null;
+ }
+
+ public Header getContentEncoding() {
+ return null;
+ }
+
+ public InputStream getContent() throws IOException {
+ return null;
+ }
+
+ public void writeTo(final OutputStream out) throws IOException {
+ // We signal that the outputstream is available
+ mStreamingReadyLock.lock();
+ mOutputStream = out;
+ mStreamingReady.signal();
+ mStreamingReadyLock.unlock();
+
+ // We then loop waiting on messages to process.
+ boolean finished = false;
+ while (!finished) {
+ DataPacket packet = mBuffer.get();
+ if (packet == null) {
+ finished = true;
+ } else {
+ write(packet);
+ }
+ mSignal.packetConsumed();
+ mConnectionFailedLock.lock();
+ if (mConnectionFailed) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "stopping loop on error");
+ }
+ finished = true;
+ }
+ mConnectionFailedLock.unlock();
+ }
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "flushing the outputstream...");
+ }
+ mOutputStream.flush();
+ }
+
+ public boolean isStreaming() {
+ return true;
+ }
+
+ public void consumeContent() throws IOException {
+ // Nothing to release
+ }
+
+ // local methods
+
+ private void write(DataPacket packet) {
+ try {
+ if (mOutputStream == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "NO OUTPUT STREAM !!!");
+ }
+ return;
+ }
+ mOutputStream.write(packet.getBytes(), 0, packet.getLength());
+ mOutputStream.flush();
+ } catch (IOException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "exc: " + e);
+ }
+ mConnectionFailedLock.lock();
+ mConnectionFailed = true;
+ mConnectionFailedLock.unlock();
+ }
+ }
+
+ public boolean isReady() {
+ mStreamingReadyLock.lock();
+ try {
+ if (mOutputStream == null) {
+ mStreamingReady.await();
+ }
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException in "
+ + "StreamEntity::isReady() : ", e);
+ }
+ } finally {
+ mStreamingReadyLock.unlock();
+ }
+ if (mOutputStream == null) {
+ return false;
+ }
+ return true;
+ }
+
+ public void signalOutputStream() {
+ mStreamingReadyLock.lock();
+ mStreamingReady.signal();
+ mStreamingReadyLock.unlock();
+ }
+ }
+
+ /**
+ * Initialize mBridgeThread using the TLS value of
+ * Thread.currentThread(). Called on start up of the native child
+ * thread.
+ */
+ public synchronized void initChildThread() {
+ mBridgeThread = Thread.currentThread();
+ }
+
+ public void setContentLength(long length) {
+ mContentLength = length;
+ }
+
+ /**
+ * Analagous to the native-side HttpRequest::open() function. This
+ * initializes an underlying HttpClient method, but does
+ * not go to the wire. On success, this enables a call to send() to
+ * initiate the transaction.
+ *
+ * @param method The HTTP method, e.g GET or POST.
+ * @param url The URL to open.
+ * @return True on success with a complete HTTP response.
+ * False on failure.
+ */
+ public synchronized boolean open(String method, String url) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "open " + method + " " + url);
+ }
+ // Create the client
+ if (mConnectionFailed) {
+ // interrupt() could have been called even before open()
+ return false;
+ }
+ mClient = new DefaultHttpClient();
+ mClient.setHttpRequestRetryHandler(
+ new DefaultHttpRequestRetryHandler(0, false));
+ mBodyInputStream = null;
+ mResponseLine = null;
+ mResponseHeaders = null;
+ mPostEntity = null;
+ mHttpThread = null;
+ mConnectionFailed = false;
+ mConnectionFinished = false;
+
+ // Create the method. We support everything that
+ // Apache HttpClient supports, apart from TRACE.
+ if ("GET".equalsIgnoreCase(method)) {
+ mMethod = new HttpGet(url);
+ } else if ("POST".equalsIgnoreCase(method)) {
+ mMethod = new HttpPost(url);
+ mPostEntity = new StreamEntity();
+ ((HttpPost)mMethod).setEntity(mPostEntity);
+ } else if ("HEAD".equalsIgnoreCase(method)) {
+ mMethod = new HttpHead(url);
+ } else if ("PUT".equalsIgnoreCase(method)) {
+ mMethod = new HttpPut(url);
+ } else if ("DELETE".equalsIgnoreCase(method)) {
+ mMethod = new HttpDelete(url);
+ } else {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Method " + method + " not supported");
+ }
+ return false;
+ }
+ HttpParams params = mClient.getParams();
+ // We handle the redirections C++-side
+ HttpClientParams.setRedirecting(params, false);
+ HttpProtocolParams.setUseExpectContinue(params, false);
+ return true;
+ }
+
+ /**
+ * We use this to start the connection thread (doing the method execute).
+ * We usually always return true here, as the connection will run its
+ * course in the thread.
+ * We only return false if interrupted beforehand -- if a connection
+ * problem happens, we will thus fail in either sendPostData() or
+ * parseHeaders().
+ */
+ public synchronized boolean connectToRemote() {
+ boolean ret = false;
+ applyRequestHeaders();
+ mConnectionFailedLock.lock();
+ if (!mConnectionFailed) {
+ mHttpThread = new Thread(new Connection());
+ mHttpThread.start();
+ }
+ ret = mConnectionFailed;
+ mConnectionFailedLock.unlock();
+ return !ret;
+ }
+
+ /**
+ * Get the complete response line of the HTTP request. Only valid on
+ * completion of the transaction.
+ * @return The complete HTTP response line, e.g "HTTP/1.0 200 OK".
+ */
+ public synchronized String getResponseLine() {
+ return mResponseLine;
+ }
+
+ /**
+ * Wait for the request thread completion
+ * (unless already finished)
+ */
+ private void waitUntilConnectionFinished() {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "waitUntilConnectionFinished("
+ + mConnectionFinished + ")");
+ }
+ if (!mConnectionFinished) {
+ if (mHttpThread != null) {
+ try {
+ mHttpThread.join();
+ mConnectionFinished = true;
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "http thread joined");
+ }
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "interrupted: " + e);
+ }
+ }
+ } else {
+ Log.e(LOG_TAG, ">>> Trying to join on mHttpThread " +
+ "when it does not exist!");
+ }
+ }
+ }
+
+ // Headers handling
+
+ /**
+ * Receive all headers from the server and populate
+ * mResponseHeaders.
+ * @return True if headers are successfully received, False on
+ * connection error.
+ */
+ public synchronized boolean parseHeaders() {
+ mConnectionFailedLock.lock();
+ if (mConnectionFailed) {
+ mConnectionFailedLock.unlock();
+ return false;
+ }
+ mConnectionFailedLock.unlock();
+ waitUntilConnectionFinished();
+ mResponseHeaders = new HashMap<String, String[]>();
+ if (mResponse == null)
+ return false;
+
+ Header[] headers = mResponse.getAllHeaders();
+ for (int i = 0; i < headers.length; i++) {
+ Header header = headers[i];
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "header " + header.getName()
+ + " -> " + header.getValue());
+ }
+ setResponseHeader(header.getName(), header.getValue());
+ }
+
+ return true;
+ }
+
+ /**
+ * Set a header to send with the HTTP request. Will not take effect
+ * on a transaction already in progress. The key is associated
+ * case-insensitive, but stored case-sensitive.
+ * @param name The name of the header, e.g "Set-Cookie".
+ * @param value The value for this header, e.g "text/html".
+ */
+ public synchronized void setRequestHeader(String name, String value) {
+ String[] mapValue = { name, value };
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "setRequestHeader: " + name + " => " + value);
+ }
+ if (name.equalsIgnoreCase(KEY_CONTENT_LENGTH)) {
+ setContentLength(Long.parseLong(value));
+ } else {
+ mRequestHeaders.put(name.toLowerCase(), mapValue);
+ }
+ }
+
+ /**
+ * Returns the value associated with the given request header.
+ * @param name The name of the request header, non-null, case-insensitive.
+ * @return The value associated with the request header, or null if
+ * not set, or error.
+ */
+ public synchronized String getRequestHeader(String name) {
+ String[] value = mRequestHeaders.get(name.toLowerCase());
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ }
+
+ private void applyRequestHeaders() {
+ if (mMethod == null)
+ return;
+ Iterator<String[]> it = mRequestHeaders.values().iterator();
+ while (it.hasNext()) {
+ // Set the key case-sensitive.
+ String[] entry = it.next();
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "apply header " + entry[HEADERS_MAP_INDEX_KEY] +
+ " => " + entry[HEADERS_MAP_INDEX_VALUE]);
+ }
+ mMethod.setHeader(entry[HEADERS_MAP_INDEX_KEY],
+ entry[HEADERS_MAP_INDEX_VALUE]);
+ }
+ }
+
+ /**
+ * Returns the value associated with the given response header.
+ * @param name The name of the response header, non-null, case-insensitive.
+ * @return The value associated with the response header, or null if
+ * not set or error.
+ */
+ public synchronized String getResponseHeader(String name) {
+ if (mResponseHeaders != null) {
+ String[] value = mResponseHeaders.get(name.toLowerCase());
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ } else {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "getResponseHeader() called but "
+ + "response not received");
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Return all response headers, separated by CR-LF line endings, and
+ * ending with a trailing blank line. This mimics the format of the
+ * raw response header up to but not including the body.
+ * @return A string containing the entire response header.
+ */
+ public synchronized String getAllResponseHeaders() {
+ if (mResponseHeaders == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "getAllResponseHeaders() called but "
+ + "response not received");
+ }
+ return null;
+ }
+ StringBuilder result = new StringBuilder();
+ Iterator<String[]> it = mResponseHeaders.values().iterator();
+ while (it.hasNext()) {
+ String[] entry = it.next();
+ // Output the "key: value" lines.
+ result.append(entry[HEADERS_MAP_INDEX_KEY]);
+ result.append(": ");
+ result.append(entry[HEADERS_MAP_INDEX_VALUE]);
+ result.append(HTTP_LINE_ENDING);
+ }
+ result.append(HTTP_LINE_ENDING);
+ return result.toString();
+ }
+
+
+ /**
+ * Set a response header and associated value. The key is associated
+ * case-insensitively, but stored case-sensitively.
+ * @param name Case sensitive request header key.
+ * @param value The associated value.
+ */
+ private void setResponseHeader(String name, String value) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Set response header " + name + ": " + value);
+ }
+ String mapValue[] = { name, value };
+ mResponseHeaders.put(name.toLowerCase(), mapValue);
+ }
+
+ // Cookie handling
+
+ /**
+ * Get the cookie for the given URL.
+ * @param url The fully qualified URL.
+ * @return A string containing the cookie for the URL if it exists,
+ * or null if not.
+ */
+ public static String getCookieForUrl(String url) {
+ // Get the cookie for this URL, set as a header
+ return CookieManager.getInstance().getCookie(url);
+ }
+
+ /**
+ * Set the cookie for the given URL.
+ * @param url The fully qualified URL.
+ * @param cookie The new cookie value.
+ * @return A string containing the cookie for the URL if it exists,
+ * or null if not.
+ */
+ public static void setCookieForUrl(String url, String cookie) {
+ // Get the cookie for this URL, set as a header
+ CookieManager.getInstance().setCookie(url, cookie);
+ }
+
+ // Cache handling
+
+ /**
+ * Perform a request using LocalServer if possible. Initializes
+ * class members so that receive() will obtain data from the stream
+ * provided by the response.
+ * @param url The fully qualified URL to try in LocalServer.
+ * @return True if the url was found and is now setup to receive.
+ * False if not found, with no side-effect.
+ */
+ public synchronized boolean useLocalServerResult(String url) {
+ UrlInterceptHandlerGears handler =
+ UrlInterceptHandlerGears.getInstance();
+ if (handler == null) {
+ return false;
+ }
+ UrlInterceptHandlerGears.ServiceResponse serviceResponse =
+ handler.getServiceResponse(url, mRequestHeaders);
+ if (serviceResponse == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "No response in LocalServer");
+ }
+ return false;
+ }
+ // LocalServer will handle this URL. Initialize stream and
+ // response.
+ mBodyInputStream = serviceResponse.getInputStream();
+ mResponseLine = serviceResponse.getStatusLine();
+ mResponseHeaders = serviceResponse.getResponseHeaders();
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Got response from LocalServer: " + mResponseLine);
+ }
+ return true;
+ }
+
+ /**
+ * Perform a request using the cache result if present. Initializes
+ * class members so that receive() will obtain data from the cache.
+ * @param url The fully qualified URL to try in the cache.
+ * @return True is the url was found and is now setup to receive
+ * from cache. False if not found, with no side-effect.
+ */
+ public synchronized boolean useCacheResult(String url) {
+ // Try the browser's cache. CacheManager wants a Map<String, String>.
+ Map<String, String> cacheRequestHeaders = new HashMap<String, String>();
+ Iterator<Map.Entry<String, String[]>> it =
+ mRequestHeaders.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, String[]> entry = it.next();
+ cacheRequestHeaders.put(
+ entry.getKey(),
+ entry.getValue()[HEADERS_MAP_INDEX_VALUE]);
+ }
+ CacheResult mCacheResult =
+ CacheManager.getCacheFile(url, cacheRequestHeaders);
+ if (mCacheResult == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "No CacheResult for " + url);
+ }
+ return false;
+ }
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Got CacheResult from browser cache");
+ }
+ // Check for expiry. -1 is "never", otherwise milliseconds since 1970.
+ // Can be compared to System.currentTimeMillis().
+ long expires = mCacheResult.getExpires();
+ if (expires >= 0 && System.currentTimeMillis() >= expires) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "CacheResult expired "
+ + (System.currentTimeMillis() - expires)
+ + " milliseconds ago");
+ }
+ // Cache hit has expired. Do not return it.
+ return false;
+ }
+ // Setup the mBodyInputStream to come from the cache.
+ mBodyInputStream = mCacheResult.getInputStream();
+ if (mBodyInputStream == null) {
+ // Cache result may have gone away.
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "No mBodyInputStream for CacheResult " + url);
+ }
+ return false;
+ }
+ // Cache hit. Parse headers.
+ synthesizeHeadersFromCacheResult(mCacheResult);
+ return true;
+ }
+
+ /**
+ * Take the limited set of headers in a CacheResult and synthesize
+ * response headers.
+ * @param cacheResult A CacheResult to populate mResponseHeaders with.
+ */
+ private void synthesizeHeadersFromCacheResult(CacheResult cacheResult) {
+ int statusCode = cacheResult.getHttpStatusCode();
+ // The status message is informal, so we can greatly simplify it.
+ String statusMessage;
+ if (statusCode >= 200 && statusCode < 300) {
+ statusMessage = "OK";
+ } else if (statusCode >= 300 && statusCode < 400) {
+ statusMessage = "MOVED";
+ } else {
+ statusMessage = "UNAVAILABLE";
+ }
+ // Synthesize the response line.
+ mResponseLine = "HTTP/1.1 " + statusCode + " " + statusMessage;
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Synthesized " + mResponseLine);
+ }
+ // Synthesize the returned headers from cache.
+ mResponseHeaders = new HashMap<String, String[]>();
+ String contentLength = Long.toString(cacheResult.getContentLength());
+ setResponseHeader(KEY_CONTENT_LENGTH, contentLength);
+ long expires = cacheResult.getExpires();
+ if (expires >= 0) {
+ // "Expires" header is valid and finite. Milliseconds since 1970
+ // epoch, formatted as RFC-1123.
+ String expiresString = DateUtils.formatDate(new Date(expires));
+ setResponseHeader(KEY_EXPIRES, expiresString);
+ }
+ String lastModified = cacheResult.getLastModified();
+ if (lastModified != null) {
+ // Last modification time of the page. Passed end-to-end, but
+ // not used by us.
+ setResponseHeader(KEY_LAST_MODIFIED, lastModified);
+ }
+ String eTag = cacheResult.getETag();
+ if (eTag != null) {
+ // Entity tag. A kind of GUID to identify identical resources.
+ setResponseHeader(KEY_ETAG, eTag);
+ }
+ String location = cacheResult.getLocation();
+ if (location != null) {
+ // If valid, refers to the location of a redirect.
+ setResponseHeader(KEY_LOCATION, location);
+ }
+ String mimeType = cacheResult.getMimeType();
+ if (mimeType == null) {
+ // Use a safe default MIME type when none is
+ // specified. "text/plain" is safe to render in the browser
+ // window (even if large) and won't be intepreted as anything
+ // that would cause execution.
+ mimeType = DEFAULT_MIME_TYPE;
+ }
+ String encoding = cacheResult.getEncoding();
+ // Encoding may not be specified. No default.
+ String contentType = mimeType;
+ if (encoding != null) {
+ if (encoding.length() > 0) {
+ contentType += "; charset=" + encoding;
+ }
+ }
+ setResponseHeader(KEY_CONTENT_TYPE, contentType);
+ }
+
+ /**
+ * Create a CacheResult for this URL. This enables the repsonse body
+ * to be sent in calls to appendCacheResult().
+ * @param url The fully qualified URL to add to the cache.
+ * @param responseCode The response code returned for the request, e.g 200.
+ * @param mimeType The MIME type of the body, e.g "text/plain".
+ * @param encoding The encoding, e.g "utf-8". Use "" for unknown.
+ */
+ public synchronized boolean createCacheResult(
+ String url, int responseCode, String mimeType, String encoding) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Making cache entry for " + url);
+ }
+ // Take the headers and parse them into a format needed by
+ // CacheManager.
+ Headers cacheHeaders = new Headers();
+ Iterator<Map.Entry<String, String[]>> it =
+ mResponseHeaders.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, String[]> entry = it.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.
+ cacheHeaders.parseHeader(buffer);
+ }
+ mCacheResult = CacheManager.createCacheFile(
+ url, responseCode, cacheHeaders, mimeType, true);
+ if (mCacheResult != null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Saving into cache");
+ }
+ mCacheResult.setEncoding(encoding);
+ mCacheResultUrl = url;
+ return true;
+ } else {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Couldn't create mCacheResult");
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Add data from the response body to the CacheResult created with
+ * createCacheResult().
+ * @param data A byte array of the next sequential bytes in the
+ * response body.
+ * @param bytes The number of bytes to write from the start of
+ * the array.
+ * @return True if all bytes successfully written, false on failure.
+ */
+ public synchronized boolean appendCacheResult(byte[] data, int bytes) {
+ if (mCacheResult == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "appendCacheResult() called without a "
+ + "CacheResult initialized");
+ }
+ return false;
+ }
+ try {
+ mCacheResult.getOutputStream().write(data, 0, bytes);
+ } catch (IOException ex) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Got IOException writing cache data: " + ex);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Save the completed CacheResult into the CacheManager. This must
+ * have been created first with createCacheResult().
+ * @return Returns true if the entry has been successfully saved.
+ */
+ public synchronized boolean saveCacheResult() {
+ if (mCacheResult == null || mCacheResultUrl == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Tried to save cache result but "
+ + "createCacheResult not called");
+ }
+ return false;
+ }
+
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Saving cache result");
+ }
+ CacheManager.saveCacheFile(mCacheResultUrl, mCacheResult);
+ mCacheResult = null;
+ mCacheResultUrl = null;
+ return true;
+ }
+
+
+ /**
+ * Interrupt a blocking IO operation. This will cause the child
+ * thread to expediently return from an operation if it was stuck at
+ * the time. Note that this inherently races, and unfortunately
+ * requires the caller to loop.
+ */
+ public synchronized void interrupt() {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "INTERRUPT CALLED");
+ }
+ mConnectionFailedLock.lock();
+ mConnectionFailed = true;
+ mConnectionFailedLock.unlock();
+ if (mMethod != null) {
+ mMethod.abort();
+ }
+ if (mHttpThread != null) {
+ waitUntilConnectionFinished();
+ }
+ }
+
+ /**
+ * Receive the next sequential bytes of the response body after
+ * successful connection. This will receive up to the size of the
+ * provided byte array. If there is no body, this will return 0
+ * bytes on the first call after connection.
+ * @param buf A pre-allocated byte array to receive data into.
+ * @return The number of bytes from the start of the array which
+ * have been filled, 0 on EOF, or negative on error.
+ */
+ public synchronized int receive(byte[] buf) {
+ if (mBodyInputStream == null) {
+ // If this is the first call, setup the InputStream. This may
+ // fail if there were headers, but no body returned by the
+ // server.
+ try {
+ if (mResponse != null) {
+ HttpEntity entity = mResponse.getEntity();
+ mBodyInputStream = entity.getContent();
+ }
+ } catch (IOException inputException) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Failed to connect InputStream: "
+ + inputException);
+ }
+ // Not unexpected. For example, 404 response return headers,
+ // and sometimes a body with a detailed error.
+ }
+ if (mBodyInputStream == null) {
+ // No error stream either. Treat as a 0 byte response.
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "No InputStream");
+ }
+ return 0; // EOF.
+ }
+ }
+ int ret;
+ try {
+ int got = mBodyInputStream.read(buf);
+ if (got > 0) {
+ // Got some bytes, not EOF.
+ ret = got;
+ } else {
+ // EOF.
+ mBodyInputStream.close();
+ ret = 0;
+ }
+ } catch (IOException e) {
+ // An abort() interrupts us by calling close() on our stream.
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Got IOException in mBodyInputStream.read(): ", e);
+ }
+ ret = -1;
+ }
+ return ret;
+ }
+
+ /**
+ * For POST method requests, send a stream of data provided by the
+ * native side in repeated callbacks.
+ * We put the data in mBuffer, and wait until it is consumed
+ * by the StreamEntity in the request thread.
+ * @param data A byte array containing the data to sent, or null
+ * if indicating EOF.
+ * @param bytes The number of bytes from the start of the array to
+ * send, or 0 if indicating EOF.
+ * @return True if all bytes were successfully sent, false on error.
+ */
+ public boolean sendPostData(byte[] data, int bytes) {
+ mConnectionFailedLock.lock();
+ if (mConnectionFailed) {
+ mConnectionFailedLock.unlock();
+ return false;
+ }
+ mConnectionFailedLock.unlock();
+ if (mPostEntity == null) return false;
+
+ // We block until the outputstream is available
+ // (or in case of connection error)
+ if (!mPostEntity.isReady()) return false;
+
+ if (data == null && bytes == 0) {
+ mBuffer.put(null);
+ } else {
+ mBuffer.put(new DataPacket(data, bytes));
+ }
+ mSignal.waitUntilPacketConsumed();
+
+ mConnectionFailedLock.lock();
+ if (mConnectionFailed) {
+ Log.e(LOG_TAG, "failure");
+ mConnectionFailedLock.unlock();
+ return false;
+ }
+ mConnectionFailedLock.unlock();
+ return true;
+ }
+
+}
diff --git a/core/java/android/webkit/gears/DesktopAndroid.java b/core/java/android/webkit/gears/DesktopAndroid.java
new file mode 100644
index 0000000..ee8ca49
--- /dev/null
+++ b/core/java/android/webkit/gears/DesktopAndroid.java
@@ -0,0 +1,109 @@
+// 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.content.Context;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.WebView;
+
+/**
+ * Utility class to create a shortcut on Android
+ */
+public class DesktopAndroid {
+
+ private static final String TAG = "Gears-J-Desktop";
+ private static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate";
+ private static final String ACTION_INSTALL_SHORTCUT =
+ "com.android.launcher.action.INSTALL_SHORTCUT";
+
+ // Android now enforces a 64x64 limit for the icon
+ private static int MAX_WIDTH = 64;
+ private static int MAX_HEIGHT = 64;
+
+ /**
+ * Small utility function returning a Bitmap object.
+ *
+ * @param path the icon path
+ */
+ private static Bitmap getBitmap(String path) {
+ return BitmapFactory.decodeFile(path);
+ }
+
+ /**
+ * Create a shortcut for a webpage.
+ *
+ * <p>To set a shortcut on Android, we use the ACTION_INSTALL_SHORTCUT
+ * from the default Home application. We only have to create an Intent
+ * containing extra parameters specifying the shortcut.
+ * <p>Note: the shortcut mechanism is not system wide and depends on the
+ * Home application; if phone carriers decide to rewrite a Home application
+ * that does not accept this Intent, no shortcut will be added.
+ *
+ * @param webview the webview we are called from
+ * @param title the shortcut's title
+ * @param url the shortcut's url
+ * @param imagePath the local path of the shortcut's icon
+ */
+ public static void setShortcut(WebView webview, String title,
+ String url, String imagePath) {
+ Context context = webview.getContext();
+
+ Intent viewWebPage = new Intent(Intent.ACTION_VIEW);
+ viewWebPage.setData(Uri.parse(url));
+ viewWebPage.addCategory(Intent.CATEGORY_BROWSABLE);
+
+ Intent intent = new Intent(ACTION_INSTALL_SHORTCUT);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, viewWebPage);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
+
+ // We disallow the creation of duplicate shortcuts (i.e. same
+ // url, same title, but different screen position).
+ intent.putExtra(EXTRA_SHORTCUT_DUPLICATE, false);
+
+ Bitmap bmp = getBitmap(imagePath);
+ if (bmp != null) {
+ if ((bmp.getWidth() > MAX_WIDTH) ||
+ (bmp.getHeight() > MAX_HEIGHT)) {
+ Bitmap scaledBitmap = Bitmap.createScaledBitmap(bmp,
+ MAX_WIDTH, MAX_HEIGHT, true);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaledBitmap);
+ } else {
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bmp);
+ }
+ } else {
+ // This should not happen as we just downloaded the icon
+ Log.e(TAG, "icon file <" + imagePath + "> not found");
+ }
+
+ context.sendBroadcast(intent);
+ }
+
+}
diff --git a/core/java/android/webkit/gears/NativeDialog.java b/core/java/android/webkit/gears/NativeDialog.java
new file mode 100644
index 0000000..9e2b375
--- /dev/null
+++ b/core/java/android/webkit/gears/NativeDialog.java
@@ -0,0 +1,142 @@
+// 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.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import java.io.File;
+import java.lang.InterruptedException;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Utility class to call a modal native dialog on Android
+ * The dialog itself is an Activity defined in the Browser.
+ * @hide
+ */
+public class NativeDialog {
+
+ private static final String TAG = "Gears-J-NativeDialog";
+
+ private final String DIALOG_PACKAGE = "com.android.browser";
+ private final String DIALOG_CLASS = DIALOG_PACKAGE + ".GearsNativeDialog";
+
+ private static Lock mLock = new ReentrantLock();
+ private static Condition mDialogFinished = mLock.newCondition();
+ private static String mResults = null;
+
+ private static boolean mAsynchronousDialog;
+
+ /**
+ * Utility function to build the intent calling the
+ * dialog activity
+ */
+ private Intent createIntent(String type, String arguments) {
+ Intent intent = new Intent();
+ intent.setClassName(DIALOG_PACKAGE, DIALOG_CLASS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra("dialogArguments", arguments);
+ intent.putExtra("dialogType", type);
+ return intent;
+ }
+
+ /**
+ * Opens a native dialog synchronously and waits for its completion.
+ *
+ * The dialog is an activity (GearsNativeDialog) provided by the Browser
+ * that we call via startActivity(). Contrary to a normal activity though,
+ * we need to block until it returns. To do so, we define a static lock
+ * object in this class, which GearsNativeDialog can unlock once done
+ */
+ public String showDialog(Context context, String file,
+ String arguments) {
+
+ try {
+ mAsynchronousDialog = false;
+ mLock.lock();
+ File path = new File(file);
+ String fileName = path.getName();
+ String type = fileName.substring(0, fileName.indexOf(".html"));
+ Intent intent = createIntent(type, arguments);
+
+ mResults = null;
+ context.startActivity(intent);
+ mDialogFinished.await();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "exception e: " + e);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "exception e: " + e);
+ } finally {
+ mLock.unlock();
+ }
+
+ return mResults;
+ }
+
+ /**
+ * Opens a native dialog asynchronously
+ *
+ * The dialog is an activity (GearsNativeDialog) provided by the
+ * Browser.
+ */
+ public void showAsyncDialog(Context context, String type,
+ String arguments) {
+ mAsynchronousDialog = true;
+ Intent intent = createIntent(type, arguments);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Static method that GearsNativeDialog calls to unlock us
+ */
+ public static void signalFinishedDialog() {
+ if (!mAsynchronousDialog) {
+ mLock.lock();
+ mDialogFinished.signal();
+ mLock.unlock();
+ } else {
+ // we call the native callback
+ closeAsynchronousDialog(mResults);
+ }
+ }
+
+ /**
+ * Static method that GearsNativeDialog calls to set the
+ * dialog's result
+ */
+ public static void closeDialog(String res) {
+ mResults = res;
+ }
+
+ /**
+ * Native callback method
+ */
+ private native static void closeAsynchronousDialog(String res);
+}
diff --git a/core/java/android/webkit/gears/PluginSettings.java b/core/java/android/webkit/gears/PluginSettings.java
new file mode 100644
index 0000000..2d0cc13
--- /dev/null
+++ b/core/java/android/webkit/gears/PluginSettings.java
@@ -0,0 +1,79 @@
+// 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.content.Context;
+import android.util.Log;
+import android.webkit.Plugin;
+import android.webkit.Plugin.PreferencesClickHandler;
+
+/**
+ * Simple bridge class intercepting the click in the
+ * browser plugin list and calling the Gears settings
+ * dialog.
+ */
+public class PluginSettings {
+
+ private static final String TAG = "Gears-J-PluginSettings";
+ private Context mContext;
+
+ public PluginSettings(Plugin plugin) {
+ plugin.setClickHandler(new ClickHandler());
+ }
+
+ /**
+ * We do not call the dialog synchronously here as the main
+ * message loop would be blocked, so we call it via a secondary thread.
+ */
+ private class ClickHandler implements PreferencesClickHandler {
+ public void handleClickEvent(Context context) {
+ mContext = context.getApplicationContext();
+ Thread startDialog = new Thread(new StartDialog(context));
+ startDialog.start();
+ }
+ }
+
+ /**
+ * Simple wrapper class to call the gears native method in
+ * a separate thread (the native code will then instanciate a NativeDialog
+ * object which will start the GearsNativeDialog activity defined in
+ * the Browser).
+ */
+ private class StartDialog implements Runnable {
+ Context mContext;
+
+ public StartDialog(Context context) {
+ mContext = context;
+ }
+
+ public void run() {
+ runSettingsDialog(mContext);
+ }
+ }
+
+ private static native void runSettingsDialog(Context c);
+
+}
diff --git a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java
new file mode 100644
index 0000000..2a5cbe9
--- /dev/null
+++ b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java
@@ -0,0 +1,501 @@
+// 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);
+}
diff --git a/core/java/android/webkit/gears/VersionExtractor.java b/core/java/android/webkit/gears/VersionExtractor.java
new file mode 100644
index 0000000..172dacb
--- /dev/null
+++ b/core/java/android/webkit/gears/VersionExtractor.java
@@ -0,0 +1,147 @@
+// 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 java.io.IOException;
+import java.io.StringReader;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.FactoryConfigurationError;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.w3c.dom.Document;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * A class that can extract the Gears version and upgrade URL from an
+ * xml document.
+ */
+public final class VersionExtractor {
+
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "Gears-J-VersionExtractor";
+ /**
+ * XML element names.
+ */
+ private static final String VERSION = "em:version";
+ private static final String URL = "em:updateLink";
+
+ /**
+ * Parses the input xml string and invokes the native
+ * setVersionAndUrl method.
+ * @param xml is the XML string to parse.
+ * @return true if the extraction is successful and false otherwise.
+ */
+ public static boolean extract(String xml, long nativeObject) {
+ try {
+ // Build the builders.
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+
+ // Create the document.
+ Document doc = builder.parse(new InputSource(new StringReader(xml)));
+
+ // Look for the version and url elements and get their text
+ // contents.
+ String version = extractText(doc, VERSION);
+ String url = extractText(doc, URL);
+
+ // If we have both, let the native side know.
+ if (version != null && url != null) {
+ setVersionAndUrl(version, url, nativeObject);
+ return true;
+ }
+
+ return false;
+
+ } catch (FactoryConfigurationError ex) {
+ Log.e(TAG, "Could not create the DocumentBuilderFactory " + ex);
+ } catch (ParserConfigurationException ex) {
+ Log.e(TAG, "Could not create the DocumentBuilder " + ex);
+ } catch (SAXException ex) {
+ Log.e(TAG, "Could not parse the xml " + ex);
+ } catch (IOException ex) {
+ Log.e(TAG, "Could not read the xml " + ex);
+ }
+
+ return false;
+ }
+
+ /**
+ * Extracts the text content of the first element with the given name.
+ * @param doc is the Document where the element is searched for.
+ * @param elementName is name of the element to searched for.
+ * @return the text content of the element or null if no such
+ * element is found.
+ */
+ private static String extractText(Document doc, String elementName) {
+ String text = null;
+ NodeList node_list = doc.getElementsByTagName(elementName);
+
+ if (node_list.getLength() > 0) {
+ // We are only interested in the first node. Normally there
+ // should not be more than one anyway.
+ Node node = node_list.item(0);
+
+ // Iterate through the text children.
+ NodeList child_list = node.getChildNodes();
+
+ try {
+ for (int i = 0; i < child_list.getLength(); ++i) {
+ Node child = child_list.item(i);
+ if (child.getNodeType() == Node.TEXT_NODE) {
+ if (text == null) {
+ text = new String();
+ }
+ text += child.getNodeValue();
+ }
+ }
+ } catch (DOMException ex) {
+ Log.e(TAG, "getNodeValue() failed " + ex);
+ }
+ }
+
+ if (text != null) {
+ text = text.trim();
+ }
+
+ return text;
+ }
+
+ /**
+ * Native method used to send the version and url back to the C++
+ * side.
+ */
+ private static native void setVersionAndUrl(
+ String version, String url, long nativeObject);
+}
diff --git a/core/java/android/webkit/gears/ZipInflater.java b/core/java/android/webkit/gears/ZipInflater.java
new file mode 100644
index 0000000..f6b6be5
--- /dev/null
+++ b/core/java/android/webkit/gears/ZipInflater.java
@@ -0,0 +1,200 @@
+// 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.os.StatFs;
+import android.util.Log;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+
+/**
+ * A class that can inflate a zip archive.
+ */
+public final class ZipInflater {
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "Gears-J-ZipInflater";
+
+ /**
+ * The size of the buffer used to read from the archive.
+ */
+ private static final int BUFFER_SIZE_BYTES = 32 * 1024; // 32 KB.
+ /**
+ * The path navigation component (i.e. "../").
+ */
+ private static final String PATH_NAVIGATION_COMPONENT = ".." + File.separator;
+ /**
+ * The root of the data partition.
+ */
+ private static final String DATA_PARTITION_ROOT = "/data";
+
+ /**
+ * We need two be able to store two versions of gears in parallel:
+ * - the zipped version
+ * - the unzipped version, which will be loaded next time the browser is started.
+ * We are conservative and do not attempt to unpack unless there enough free
+ * space on the device to store 4 times the new Gears size.
+ */
+ private static final long SIZE_MULTIPLIER = 4;
+
+ /**
+ * Unzips the archive with the given name.
+ * @param filename is the name of the zip to inflate.
+ * @param path is the path where the zip should be unpacked. It must contain
+ * a trailing separator, or the extraction will fail.
+ * @return true if the extraction is successful and false otherwise.
+ */
+ public static boolean inflate(String filename, String path) {
+ Log.i(TAG, "Extracting " + filename + " to " + path);
+
+ // Check that the path ends with a separator.
+ if (!path.endsWith(File.separator)) {
+ Log.e(TAG, "Path missing trailing separator: " + path);
+ return false;
+ }
+
+ boolean result = false;
+
+ // Use a ZipFile to get an enumeration of the entries and
+ // calculate the overall uncompressed size of the archive. Also
+ // check for existing files or directories that have the same
+ // name as the entries in the archive. Also check for invalid
+ // entry names (e.g names that attempt to navigate to the
+ // parent directory).
+ ZipInputStream zipStream = null;
+ long uncompressedSize = 0;
+ try {
+ ZipFile zipFile = new ZipFile(filename);
+ try {
+ Enumeration entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = (ZipEntry) entries.nextElement();
+ uncompressedSize += entry.getSize();
+ // Check against entry names that may attempt to navigate
+ // out of the destination directory.
+ if (entry.getName().indexOf(PATH_NAVIGATION_COMPONENT) >= 0) {
+ throw new IOException("Illegal entry name: " + entry.getName());
+ }
+
+ // Check against entries with the same name as pre-existing files or
+ // directories.
+ File file = new File(path + entry.getName());
+ if (file.exists()) {
+ // A file or directory with the same name already exist.
+ // This must not happen, so we treat this as an error.
+ throw new IOException(
+ "A file or directory with the same name already exists.");
+ }
+ }
+ } finally {
+ zipFile.close();
+ }
+
+ Log.i(TAG, "Determined uncompressed size: " + uncompressedSize);
+ // Check we have enough space to unpack this archive.
+ if (freeSpace() <= uncompressedSize * SIZE_MULTIPLIER) {
+ throw new IOException("Not enough space to unpack this archive.");
+ }
+
+ zipStream = new ZipInputStream(
+ new BufferedInputStream(new FileInputStream(filename)));
+ ZipEntry entry;
+ int counter;
+ byte buffer[] = new byte[BUFFER_SIZE_BYTES];
+
+ // Iterate through the entries and write each of them to a file.
+ while ((entry = zipStream.getNextEntry()) != null) {
+ File file = new File(path + entry.getName());
+ if (entry.isDirectory()) {
+ // If the entry denotes a directory, we need to create a
+ // directory with the same name.
+ file.mkdirs();
+ } else {
+ CRC32 checksum = new CRC32();
+ BufferedOutputStream output = new BufferedOutputStream(
+ new FileOutputStream(file),
+ BUFFER_SIZE_BYTES);
+ try {
+ // Read the entry and write it to the file.
+ while ((counter = zipStream.read(buffer, 0, BUFFER_SIZE_BYTES)) !=
+ -1) {
+ output.write(buffer, 0, counter);
+ checksum.update(buffer, 0, counter);
+ }
+ output.flush();
+ } finally {
+ output.close();
+ }
+
+ if (checksum.getValue() != entry.getCrc()) {
+ throw new IOException(
+ "Integrity check failed for: " + entry.getName());
+ }
+ }
+ zipStream.closeEntry();
+ }
+
+ result = true;
+
+ } catch (FileNotFoundException ex) {
+ Log.e(TAG, "The zip file could not be found. " + ex);
+ } catch (IOException ex) {
+ Log.e(TAG, "Could not read or write an entry. " + ex);
+ } catch(IllegalArgumentException ex) {
+ Log.e(TAG, "Could not create the BufferedOutputStream. " + ex);
+ } finally {
+ if (zipStream != null) {
+ try {
+ zipStream.close();
+ } catch (IOException ex) {
+ // Ignored.
+ }
+ }
+ // Discard any exceptions and return the result to the native side.
+ return result;
+ }
+ }
+
+ private static final long freeSpace() {
+ StatFs data_partition = new StatFs(DATA_PARTITION_ROOT);
+ long freeSpace = data_partition.getAvailableBlocks() *
+ data_partition.getBlockSize();
+ Log.i(TAG, "Free space on the data partition: " + freeSpace);
+ return freeSpace;
+ }
+}
diff --git a/core/java/android/webkit/gears/package.html b/core/java/android/webkit/gears/package.html
new file mode 100644
index 0000000..db6f78b
--- /dev/null
+++ b/core/java/android/webkit/gears/package.html
@@ -0,0 +1,3 @@
+<body>
+{@hide}
+</body> \ No newline at end of file