diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | 54b6cfa9a9e5b861a9930af873580d6dc20f773c (patch) | |
tree | 35051494d2af230dce54d6b31c6af8fc24091316 /core/java/android/webkit/gears | |
download | frameworks_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.java | 156 | ||||
-rw-r--r-- | core/java/android/webkit/gears/AndroidRadioDataProvider.java | 244 | ||||
-rw-r--r-- | core/java/android/webkit/gears/DesktopAndroid.java | 113 | ||||
-rw-r--r-- | core/java/android/webkit/gears/GearsPluginSettings.java | 95 | ||||
-rw-r--r-- | core/java/android/webkit/gears/HtmlDialogAndroid.java | 174 | ||||
-rw-r--r-- | core/java/android/webkit/gears/HttpRequestAndroid.java | 730 | ||||
-rw-r--r-- | core/java/android/webkit/gears/IGearsDialogService.java | 107 | ||||
-rw-r--r-- | core/java/android/webkit/gears/UrlInterceptHandlerGears.java | 497 | ||||
-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, 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 |