diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/webkit/gears | |
parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
download | frameworks_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.java | 156 | ||||
-rw-r--r-- | core/java/android/webkit/gears/AndroidRadioDataProvider.java | 244 | ||||
-rw-r--r-- | core/java/android/webkit/gears/AndroidWifiDataProvider.java | 136 | ||||
-rw-r--r-- | core/java/android/webkit/gears/ApacheHttpRequestAndroid.java | 1122 | ||||
-rw-r--r-- | core/java/android/webkit/gears/DesktopAndroid.java | 109 | ||||
-rw-r--r-- | core/java/android/webkit/gears/NativeDialog.java | 142 | ||||
-rw-r--r-- | core/java/android/webkit/gears/PluginSettings.java | 79 | ||||
-rw-r--r-- | core/java/android/webkit/gears/UrlInterceptHandlerGears.java | 501 | ||||
-rw-r--r-- | core/java/android/webkit/gears/VersionExtractor.java | 147 | ||||
-rw-r--r-- | core/java/android/webkit/gears/ZipInflater.java | 200 | ||||
-rw-r--r-- | core/java/android/webkit/gears/package.html | 3 |
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 |