summaryrefslogtreecommitdiffstats
path: root/core/java/android/webkit/gears
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commit54b6cfa9a9e5b861a9930af873580d6dc20f773c (patch)
tree35051494d2af230dce54d6b31c6af8fc24091316 /core/java/android/webkit/gears
downloadframeworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.zip
frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.gz
frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.bz2
Initial Contribution
Diffstat (limited to 'core/java/android/webkit/gears')
-rw-r--r--core/java/android/webkit/gears/AndroidGpsLocationProvider.java156
-rw-r--r--core/java/android/webkit/gears/AndroidRadioDataProvider.java244
-rw-r--r--core/java/android/webkit/gears/DesktopAndroid.java113
-rw-r--r--core/java/android/webkit/gears/GearsPluginSettings.java95
-rw-r--r--core/java/android/webkit/gears/HtmlDialogAndroid.java174
-rw-r--r--core/java/android/webkit/gears/HttpRequestAndroid.java730
-rw-r--r--core/java/android/webkit/gears/IGearsDialogService.java107
-rw-r--r--core/java/android/webkit/gears/UrlInterceptHandlerGears.java497
-rw-r--r--core/java/android/webkit/gears/VersionExtractor.java147
-rw-r--r--core/java/android/webkit/gears/ZipInflater.java200
-rw-r--r--core/java/android/webkit/gears/package.html3
11 files changed, 2466 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/DesktopAndroid.java b/core/java/android/webkit/gears/DesktopAndroid.java
new file mode 100644
index 0000000..00a9a47
--- /dev/null
+++ b/core/java/android/webkit/gears/DesktopAndroid.java
@@ -0,0 +1,113 @@
+// 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 BROWSER = "com.android.browser";
+ private static final String BROWSER_ACTIVITY = BROWSER + ".BrowserActivity";
+ 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();
+
+ ComponentName browser = new ComponentName(BROWSER, BROWSER_ACTIVITY);
+
+ Intent viewWebPage = new Intent(Intent.ACTION_VIEW);
+ viewWebPage.setComponent(browser);
+ viewWebPage.setData(Uri.parse(url));
+
+ 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/GearsPluginSettings.java b/core/java/android/webkit/gears/GearsPluginSettings.java
new file mode 100644
index 0000000..d36d3fb
--- /dev/null
+++ b/core/java/android/webkit/gears/GearsPluginSettings.java
@@ -0,0 +1,95 @@
+// 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.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+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 GearsPluginSettings {
+
+ private static final String TAG = "Gears-J-GearsPluginSettings";
+ private Context context;
+
+ public GearsPluginSettings(Plugin plugin) {
+ plugin.setClickHandler(new ClickHandler());
+ }
+
+ /**
+ * We do not need to call the dialog synchronously here (doing so
+ * actually cause a lot of problems as the main message loop is also
+ * blocked), which is why we simply call it via a thread.
+ */
+ private class ClickHandler implements PreferencesClickHandler {
+ public void handleClickEvent(Context aContext) {
+ context = aContext;
+ Thread startService = new Thread(new StartService());
+ startService.run();
+ }
+ }
+
+ private static native void runSettingsDialog(Context c);
+
+ /**
+ * StartService is the runnable we use to open the dialog.
+ * We bind the service to serviceConnection; upon
+ * onServiceConnected the dialog will be called from the
+ * native side using the runSettingsDialog method.
+ */
+ private class StartService implements Runnable {
+ public void run() {
+ HtmlDialogAndroid.bindToService(context, serviceConnection);
+ }
+ }
+
+ /**
+ * ServiceConnection instance.
+ * onServiceConnected is called upon connection with the service;
+ * we can then safely open the dialog.
+ */
+ private ServiceConnection serviceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ IGearsDialogService gearsDialogService =
+ IGearsDialogService.Stub.asInterface(service);
+ HtmlDialogAndroid.setGearsDialogService(gearsDialogService);
+ runSettingsDialog(context);
+ context.unbindService(serviceConnection);
+ HtmlDialogAndroid.setGearsDialogService(null);
+ }
+ public void onServiceDisconnected(ComponentName className) {
+ HtmlDialogAndroid.setGearsDialogService(null);
+ }
+ };
+}
diff --git a/core/java/android/webkit/gears/HtmlDialogAndroid.java b/core/java/android/webkit/gears/HtmlDialogAndroid.java
new file mode 100644
index 0000000..6209ab9
--- /dev/null
+++ b/core/java/android/webkit/gears/HtmlDialogAndroid.java
@@ -0,0 +1,174 @@
+// 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.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.webkit.CacheManager;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * Utility class to call a modal HTML dialog on Android
+ */
+public class HtmlDialogAndroid {
+
+ private static final String TAG = "Gears-J-HtmlDialog";
+ private static final String DIALOG_PACKAGE = "com.android.browser";
+ private static final String DIALOG_SERVICE = DIALOG_PACKAGE
+ + ".GearsDialogService";
+ private static final String DIALOG_INTERFACE = DIALOG_PACKAGE
+ + ".IGearsDialogService";
+
+ private static IGearsDialogService gearsDialogService;
+
+ public static void setGearsDialogService(IGearsDialogService service) {
+ gearsDialogService = service;
+ }
+
+ /**
+ * Bind to the GearsDialogService.
+ */
+ public static boolean bindToService(Context context,
+ ServiceConnection serviceConnection) {
+ Intent dialogIntent = new Intent();
+ dialogIntent.setClassName(DIALOG_PACKAGE, DIALOG_SERVICE);
+ dialogIntent.setAction(DIALOG_INTERFACE);
+ return context.bindService(dialogIntent, serviceConnection,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Bind to the GearsDialogService synchronously.
+ * The service is started using our own defaultServiceConnection
+ * handler, and we wait until the handler notifies us.
+ */
+ public void synchronousBindToService(Context context) {
+ try {
+ if (bindToService(context, defaultServiceConnection)) {
+ if (gearsDialogService == null) {
+ synchronized(defaultServiceConnection) {
+ defaultServiceConnection.wait(3000); // timeout after 3s
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Log.e(TAG, "exception: " + e);
+ }
+ }
+
+ /**
+ * Read the HTML content from the disk
+ */
+ public String readHTML(String filePath) {
+ FileInputStream inputStream = null;
+ String content = "";
+ try {
+ inputStream = new FileInputStream(filePath);
+ StringBuffer out = new StringBuffer();
+ byte[] buffer = new byte[4096];
+ for (int n; (n = inputStream.read(buffer)) != -1;) {
+ out.append(new String(buffer, 0, n));
+ }
+ content = out.toString();
+ } catch (IOException e) {
+ Log.e(TAG, "exception: " + e);
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(TAG, "exception: " + e);
+ }
+ }
+ }
+ return content;
+ }
+
+ /**
+ * Open an HTML dialog synchronously and waits for its completion.
+ * The dialog is accessed through the GearsDialogService provided by
+ * the Android Browser.
+ * We can be called either directly, and then gearsDialogService will
+ * not be set and we will bind to the service synchronously, and unbind
+ * after calling the service, or called indirectly via GearsPluginSettings.
+ * In the latter case, GearsPluginSettings does the binding/unbinding.
+ */
+ public String showDialog(Context context, String htmlFilePath,
+ String arguments) {
+
+ CacheManager.endCacheTransaction();
+
+ String ret = null;
+ boolean synchronousCall = false;
+ if (gearsDialogService == null) {
+ synchronousCall = true;
+ synchronousBindToService(context);
+ }
+
+ try {
+ if (gearsDialogService != null) {
+ String htmlContent = readHTML(htmlFilePath);
+ if (htmlContent.length() > 0) {
+ ret = gearsDialogService.showDialog(htmlContent, arguments,
+ !synchronousCall);
+ }
+ } else {
+ Log.e(TAG, "Could not connect to the GearsDialogService!");
+ }
+ if (synchronousCall) {
+ context.unbindService(defaultServiceConnection);
+ gearsDialogService = null;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception: " + e);
+ gearsDialogService = null;
+ }
+
+ CacheManager.startCacheTransaction();
+
+ return ret;
+ }
+
+ private ServiceConnection defaultServiceConnection =
+ new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ synchronized (defaultServiceConnection) {
+ gearsDialogService = IGearsDialogService.Stub.asInterface(service);
+ defaultServiceConnection.notify();
+ }
+ }
+ public void onServiceDisconnected(ComponentName className) {
+ gearsDialogService = null;
+ }
+ };
+}
diff --git a/core/java/android/webkit/gears/HttpRequestAndroid.java b/core/java/android/webkit/gears/HttpRequestAndroid.java
new file mode 100644
index 0000000..8668c54
--- /dev/null
+++ b/core/java/android/webkit/gears/HttpRequestAndroid.java
@@ -0,0 +1,730 @@
+// 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.Log;
+import android.webkit.CacheManager;
+import android.webkit.CacheManager.CacheResult;
+import android.webkit.CookieManager;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.util.CharArrayBuffer;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.net.ssl.*;
+
+/**
+ * Performs the underlying HTTP/HTTPS GET and POST 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 the java.net.HttpURLConnection class 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 HttpRequestAndroid {
+ /** 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;
+
+ /** Enable/disable all logging in this class. */
+ private static boolean logEnabled = false;
+ /** The underlying HTTP or HTTPS network connection. */
+ private HttpURLConnection connection;
+ /** HTTP body stream, setup after connection. */
+ private InputStream inputStream;
+ /** The complete response line e.g "HTTP/1.0 200 OK" */
+ private String responseLine;
+ /** Request headers, as a lowercase key -> [ unmodified key, value ] map. */
+ private Map<String, String[]> requestHeaders =
+ new HashMap<String, String[]>();
+ /** Response headers, as a lowercase key -> [ unmodified key, value ] map. */
+ private Map<String, String[]> responseHeaders;
+ /** True if the child thread is in performing blocking IO. */
+ private boolean inBlockingOperation = false;
+ /** True when the thread acknowledges the abort. */
+ private boolean abortReceived = false;
+ /** The URL used for createCacheResult() */
+ private String cacheResultUrl;
+ /** CacheResult being saved into, if inserting a new cache entry. */
+ private CacheResult cacheResult;
+ /** Initialized by initChildThread(). Used to target abort(). */
+ private Thread childThread;
+
+ /**
+ * Convenience debug function. Calls Android logging mechanism.
+ * @param str String to log to the Android console.
+ */
+ private static void log(String str) {
+ if (logEnabled) {
+ Log.i(LOG_TAG, str);
+ }
+ }
+
+ /**
+ * Turn on/off logging in this class.
+ * @param on Logging enable state.
+ */
+ public static void enableLogging(boolean on) {
+ logEnabled = on;
+ }
+
+ /**
+ * Initialize childThread using the TLS value of
+ * Thread.currentThread(). Called on start up of the native child
+ * thread.
+ */
+ public synchronized void initChildThread() {
+ childThread = Thread.currentThread();
+ }
+
+ /**
+ * Analagous to the native-side HttpRequest::open() function. This
+ * initializes an underlying java.net.HttpURLConnection, 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 (logEnabled)
+ log("open " + method + " " + url);
+ // Reset the response between calls to open().
+ inputStream = null;
+ responseLine = null;
+ responseHeaders = null;
+ if (!method.equals("GET") && !method.equals("POST")) {
+ log("Method " + method + " not supported");
+ return false;
+ }
+ // Setup the connection. This doesn't go to the wire yet - it
+ // doesn't block.
+ try {
+ connection = (HttpURLConnection) new URL(url).openConnection();
+ connection.setRequestMethod(method);
+ // Manually follow redirects.
+ connection.setInstanceFollowRedirects(false);
+ // Manually cache.
+ connection.setUseCaches(false);
+ // Enable data output in POST method requests.
+ connection.setDoOutput(method.equals("POST"));
+ // Enable data input in non-HEAD method requests.
+ // TODO: HEAD requests not tested.
+ connection.setDoInput(!method.equals("HEAD"));
+ if (connection instanceof javax.net.ssl.HttpsURLConnection) {
+ // Verify the certificate matches the origin.
+ ((HttpsURLConnection) connection).setHostnameVerifier(
+ new StrictHostnameVerifier());
+ }
+ return true;
+ } catch (IOException e) {
+ log("Got IOException in open: " + e.toString());
+ return false;
+ }
+ }
+
+ /**
+ * 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 (childThread == null) {
+ log("interrupt() called but no child thread");
+ return;
+ }
+ if (inBlockingOperation) {
+ log("Interrupting blocking operation");
+ childThread.interrupt();
+ } else {
+ log("Nothing to interrupt");
+ }
+ }
+
+ /**
+ * 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 };
+ requestHeaders.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 = requestHeaders.get(name.toLowerCase());
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * 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 (responseHeaders != null) {
+ String[] value = responseHeaders.get(name.toLowerCase());
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ } else {
+ log("getResponseHeader() called but response not received");
+ return null;
+ }
+ }
+
+ /**
+ * 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 (logEnabled)
+ log("Set response header " + name + ": " + value);
+ String mapValue[] = { name, value };
+ responseHeaders.put(name.toLowerCase(), mapValue);
+ }
+
+ /**
+ * Apply the contents of the Map requestHeaders to the connection
+ * object. Calls to setRequestHeader() after this will not affect
+ * the connection.
+ */
+ private synchronized void applyRequestHeadersToConnection() {
+ Iterator<String[]> it = requestHeaders.values().iterator();
+ while (it.hasNext()) {
+ // Set the key case-sensitive.
+ String[] entry = it.next();
+ connection.setRequestProperty(
+ entry[HEADERS_MAP_INDEX_KEY],
+ entry[HEADERS_MAP_INDEX_VALUE]);
+ }
+ }
+
+ /**
+ * 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 (responseHeaders == null) {
+ log("getAllResponseHeaders() called but response not received");
+ return null;
+ }
+ String result = new String();
+ Iterator<String[]> it = responseHeaders.values().iterator();
+ while (it.hasNext()) {
+ String[] entry = it.next();
+ // Output the "key: value" lines.
+ result += entry[HEADERS_MAP_INDEX_KEY] + ": "
+ + entry[HEADERS_MAP_INDEX_VALUE] + HTTP_LINE_ENDING;
+ }
+ result += HTTP_LINE_ENDING;
+ return result;
+ }
+
+ /**
+ * 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 responseLine;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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, requestHeaders);
+ if (serviceResponse == null) {
+ log("No response in LocalServer");
+ return false;
+ }
+ // LocalServer will handle this URL. Initialize stream and
+ // response.
+ inputStream = serviceResponse.getInputStream();
+ responseLine = serviceResponse.getStatusLine();
+ responseHeaders = serviceResponse.getResponseHeaders();
+ if (logEnabled)
+ log("Got response from LocalServer: " + responseLine);
+ 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 =
+ requestHeaders.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, String[]> entry = it.next();
+ cacheRequestHeaders.put(
+ entry.getKey(),
+ entry.getValue()[HEADERS_MAP_INDEX_VALUE]);
+ }
+ CacheResult cacheResult =
+ CacheManager.getCacheFile(url, cacheRequestHeaders);
+ if (cacheResult == null) {
+ if (logEnabled)
+ log("No CacheResult for " + url);
+ return false;
+ }
+ if (logEnabled)
+ log("Got CacheResult from browser cache");
+ // Check for expiry. -1 is "never", otherwise milliseconds since 1970.
+ // Can be compared to System.currentTimeMillis().
+ long expires = cacheResult.getExpires();
+ if (expires >= 0 && System.currentTimeMillis() >= expires) {
+ log("CacheResult expired "
+ + (System.currentTimeMillis() - expires)
+ + " milliseconds ago");
+ // Cache hit has expired. Do not return it.
+ return false;
+ }
+ // Setup the inputStream to come from the cache.
+ inputStream = cacheResult.getInputStream();
+ if (inputStream == null) {
+ // Cache result may have gone away.
+ log("No inputStream for CacheResult " + url);
+ return false;
+ }
+ // Cache hit. Parse headers.
+ synthesizeHeadersFromCacheResult(cacheResult);
+ return true;
+ }
+
+ /**
+ * Take the limited set of headers in a CacheResult and synthesize
+ * response headers.
+ * @param cacheResult A CacheResult to populate responseHeaders 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.
+ responseLine = "HTTP/1.1 " + statusCode + " " + statusMessage;
+ if (logEnabled)
+ log("Synthesized " + responseLine);
+ // Synthesize the returned headers from cache.
+ responseHeaders = 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) {
+ 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 (logEnabled)
+ log("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 =
+ responseHeaders.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);
+ }
+ cacheResult = CacheManager.createCacheFile(
+ url, responseCode, cacheHeaders, mimeType, true);
+ if (cacheResult != null) {
+ if (logEnabled)
+ log("Saving into cache");
+ cacheResult.setEncoding(encoding);
+ cacheResultUrl = url;
+ return true;
+ } else {
+ log("Couldn't create cacheResult");
+ 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 (cacheResult == null) {
+ log("appendCacheResult() called without a CacheResult initialized");
+ return false;
+ }
+ try {
+ cacheResult.getOutputStream().write(data, 0, bytes);
+ } catch (IOException ex) {
+ log("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 (cacheResult == null || cacheResultUrl == null) {
+ log("Tried to save cache result but createCacheResult not called");
+ return false;
+ }
+ if (logEnabled)
+ log("Saving cache result");
+ CacheManager.saveCacheFile(cacheResultUrl, cacheResult);
+ cacheResult = null;
+ cacheResultUrl = null;
+ return true;
+ }
+
+ /**
+ * Perform an HTTP request on the network. The underlying
+ * HttpURLConnection is connected to the remote server and the
+ * response headers are received.
+ * @return True if the connection succeeded and headers have been
+ * received. False on connection failure.
+ */
+ public boolean connectToRemote() {
+ synchronized (this) {
+ // Transfer a snapshot of our internally maintained map of request
+ // headers to the connection object.
+ applyRequestHeadersToConnection();
+ // Note blocking I/O so abort() can interrupt us.
+ inBlockingOperation = true;
+ }
+ boolean success;
+ try {
+ if (logEnabled)
+ log("Connecting to remote");
+ connection.connect();
+ if (logEnabled)
+ log("Connected");
+ success = true;
+ } catch (IOException e) {
+ log("Got IOException in connect(): " + e.toString());
+ success = false;
+ } finally {
+ synchronized (this) {
+ // No longer blocking.
+ inBlockingOperation = false;
+ }
+ }
+ return success;
+ }
+
+ /**
+ * Receive all headers from the server and populate
+ * responseHeaders. This converts from the slightly odd format
+ * returned by java.net.HttpURLConnection to a simpler
+ * java.util.Map.
+ * @return True if headers are successfully received, False on
+ * connection error.
+ */
+ public synchronized boolean parseHeaders() {
+ responseHeaders = new HashMap<String, String[]>();
+ /* HttpURLConnection contains a null terminated list of
+ * key->value response pairs. If the key is null, then the value
+ * contains the complete status line. If both key and value are
+ * null for an index, we've reached the end.
+ */
+ for (int i = 0; ; ++i) {
+ String key = connection.getHeaderFieldKey(i);
+ String value = connection.getHeaderField(i);
+ if (logEnabled)
+ log("header " + key + " -> " + value);
+ if (key == null && value == null) {
+ // End of list.
+ break;
+ } else if (key == null) {
+ // The pair with null key has the complete status line in
+ // the value, e.g "HTTP/1.0 200 OK".
+ responseLine = value;
+ } else if (value != null) {
+ // If key and value are non-null, this is a response pair, e.g
+ // "Content-Length" -> "5". Use setResponseHeader() to
+ // correctly deal with case-insensitivity of the key.
+ setResponseHeader(key, value);
+ } else {
+ // The key is non-null but value is null. Unexpected
+ // condition.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 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 int receive(byte[] buf) {
+ if (inputStream == 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 {
+ inputStream = connection.getInputStream();
+ } catch (IOException inputException) {
+ log("Failed to connect InputStream: " + inputException);
+ // Not unexpected. For example, 404 response return headers,
+ // and sometimes a body with a detailed error. Try the error
+ // stream.
+ inputStream = connection.getErrorStream();
+ if (inputStream == null) {
+ // No error stream either. Treat as a 0 byte response.
+ log("No InputStream");
+ return 0; // EOF.
+ }
+ }
+ }
+ synchronized (this) {
+ // Note blocking I/O so abort() can interrupt us.
+ inBlockingOperation = true;
+ }
+ int ret;
+ try {
+ int got = inputStream.read(buf);
+ if (got > 0) {
+ // Got some bytes, not EOF.
+ ret = got;
+ } else {
+ // EOF.
+ inputStream.close();
+ ret = 0;
+ }
+ } catch (IOException e) {
+ // An abort() interrupts us by calling close() on our stream.
+ log("Got IOException in inputStream.read(): " + e.toString());
+ ret = -1;
+ } finally {
+ synchronized (this) {
+ // No longer blocking.
+ inBlockingOperation = false;
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * For POST method requests, send a stream of data provided by the
+ * native side in repeated callbacks.
+ * @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) {
+ synchronized (this) {
+ // Note blocking I/O so abort() can interrupt us.
+ inBlockingOperation = true;
+ }
+ boolean success;
+ try {
+ OutputStream outputStream = connection.getOutputStream();
+ if (data == null && bytes == 0) {
+ outputStream.close();
+ } else {
+ outputStream.write(data, 0, bytes);
+ }
+ success = true;
+ } catch (IOException e) {
+ log("Got IOException in post: " + e.toString());
+ success = false;
+ } finally {
+ synchronized (this) {
+ // No longer blocking.
+ inBlockingOperation = false;
+ }
+ }
+ return success;
+ }
+}
diff --git a/core/java/android/webkit/gears/IGearsDialogService.java b/core/java/android/webkit/gears/IGearsDialogService.java
new file mode 100644
index 0000000..82a3bd9
--- /dev/null
+++ b/core/java/android/webkit/gears/IGearsDialogService.java
@@ -0,0 +1,107 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ * Original file: android.webkit.gears/IGearsDialogService.aidl
+ */
+package android.webkit.gears;
+import java.lang.String;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Binder;
+import android.os.Parcel;
+public interface IGearsDialogService extends android.os.IInterface
+{
+/** Local-side IPC implementation stub class. */
+public static abstract class Stub extends android.os.Binder implements android.webkit.gears.IGearsDialogService
+{
+private static final java.lang.String DESCRIPTOR = "com.android.browser.IGearsDialogService";
+/** Construct the stub at attach it to the interface. */
+public Stub()
+{
+this.attachInterface(this, DESCRIPTOR);
+}
+/**
+ * Cast an IBinder object into an IGearsDialogService interface,
+ * generating a proxy if needed.
+ */
+public static android.webkit.gears.IGearsDialogService asInterface(android.os.IBinder obj)
+{
+if ((obj==null)) {
+return null;
+}
+android.webkit.gears.IGearsDialogService in = (android.webkit.gears.IGearsDialogService)obj.queryLocalInterface(DESCRIPTOR);
+if ((in!=null)) {
+return in;
+}
+return new android.webkit.gears.IGearsDialogService.Stub.Proxy(obj);
+}
+public android.os.IBinder asBinder()
+{
+return this;
+}
+public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+{
+switch (code)
+{
+case INTERFACE_TRANSACTION:
+{
+reply.writeString(DESCRIPTOR);
+return true;
+}
+case TRANSACTION_showDialog:
+{
+data.enforceInterface(DESCRIPTOR);
+java.lang.String _arg0;
+_arg0 = data.readString();
+java.lang.String _arg1;
+_arg1 = data.readString();
+boolean _arg2;
+_arg2 = (0!=data.readInt());
+java.lang.String _result = this.showDialog(_arg0, _arg1, _arg2);
+reply.writeNoException();
+reply.writeString(_result);
+return true;
+}
+}
+return super.onTransact(code, data, reply, flags);
+}
+private static class Proxy implements android.webkit.gears.IGearsDialogService
+{
+private android.os.IBinder mRemote;
+Proxy(android.os.IBinder remote)
+{
+mRemote = remote;
+}
+public android.os.IBinder asBinder()
+{
+return mRemote;
+}
+public java.lang.String getInterfaceDescriptor()
+{
+return DESCRIPTOR;
+}
+public java.lang.String showDialog(java.lang.String htmlContent, java.lang.String dialogArguments, boolean inSettings) throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+android.os.Parcel _reply = android.os.Parcel.obtain();
+java.lang.String _result;
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+_data.writeString(htmlContent);
+_data.writeString(dialogArguments);
+_data.writeInt(((inSettings)?(1):(0)));
+mRemote.transact(Stub.TRANSACTION_showDialog, _data, _reply, 0);
+_reply.readException();
+_result = _reply.readString();
+}
+finally {
+_reply.recycle();
+_data.recycle();
+}
+return _result;
+}
+}
+static final int TRANSACTION_showDialog = (IBinder.FIRST_CALL_TRANSACTION + 0);
+}
+public java.lang.String showDialog(java.lang.String htmlContent, java.lang.String dialogArguments, boolean inSettings) throws android.os.RemoteException;
+}
diff --git a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java
new file mode 100644
index 0000000..95fc30f
--- /dev/null
+++ b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java
@@ -0,0 +1,497 @@
+// 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 =
+ HttpRequestAndroid.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 =
+ HttpRequestAndroid.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(HttpRequestAndroid.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) {
+ 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) {
+ if (logEnabled)
+ 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 Android logging mechanism.
+ * @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