diff --git a/framework/src/org/apache/cordova/FileHelper.java b/framework/src/org/apache/cordova/FileHelper.java index 8b446b06..81f32f0a 100644 --- a/framework/src/org/apache/cordova/FileHelper.java +++ b/framework/src/org/apache/cordova/FileHelper.java @@ -26,10 +26,11 @@ import org.apache.cordova.api.CordovaInterface; import org.apache.cordova.api.LOG; import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URLConnection; -import java.util.Locale; +import java.io.OutputStream; public class FileHelper { private static final String LOG_TAG = "FileUtils"; @@ -90,7 +91,7 @@ public class FileHelper { * @throws IOException */ public static InputStream getInputStreamFromUriString(String uriString, CordovaInterface cordova) throws IOException { - if (uriString.startsWith("content")) { + if (uriString.startsWith("content:")) { Uri uri = Uri.parse(uriString); return cordova.getActivity().getContentResolver().openInputStream(uri); } else if (uriString.startsWith("file:///android_asset/")) { @@ -102,6 +103,57 @@ public class FileHelper { } } + public static OutputStream getOutputStreamFromUriString(String uriString, CordovaInterface cordova) throws FileNotFoundException{ + if (uriString.startsWith("content:")) { + Uri uri = Uri.parse(uriString); + return cordova.getActivity().getContentResolver().openOutputStream(uri); + } else if (uriString.startsWith("file:") && !uriString.startsWith("file:///android_asset/")) { + String realPath = uriString.substring(7); + return new FileOutputStream(realPath); + } else { + return null; + } + } + /** + * Returns whether the uri can be written to by openeing a File to that uri + * + * @param the URI to test + * @return boolean indicating whether the uri is writable + */ + public static boolean isUriWritable(String uriString) { + String scheme = uriString.split(":")[0]; + String writableSchemes[] = new String[]{ "content" }; + + if(scheme.equals("file")){ + // special case file + if(uriString.startsWith("file:///android_asset/")){ + return false; + } else { + return true; + } + } + for(int i = writableSchemes.length - 1; i >= 0 ; i--){ + if(writableSchemes[i].equals(scheme)){ + return true; + } + } + return false; + } + + /** + * Ensures the "file://" prefix exists for the given string + * If the given URI string has a "file://" prefix, it is returned unchanged + * + * @param path - the path string to operate on + * @return a String with the "file://" scheme set + */ + public static String insertFileProtocol(String path) { + if(!path.matches("^[a-z0-9+.-]+:.*")){ + path = "file://" + path; + } + return path; + } + /** * Removes the "file://" prefix from the given URI string, if applicable. * If the given URI string doesn't have a "file://" prefix, it is returned unchanged. diff --git a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java index 847972ee..c4c4d3e4 100644 --- a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java @@ -19,9 +19,10 @@ package org.apache.cordova; import java.io.IOException; -import java.io.InputStream; import org.apache.cordova.api.CordovaInterface; +import org.apache.cordova.api.DataResource; +import org.apache.cordova.api.DataResourceContext; import org.apache.cordova.api.LOG; import android.annotation.TargetApi; @@ -43,41 +44,24 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient { @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + // We need to support the new DataResource intercepts without breaking the shouldInterceptRequest mechanism. + DataResource dataResource = DataResource.initiateNewDataRequestForUri(url, this.appView.pluginManager, cordova, + new DataResourceContext("WebViewClient.shouldInterceptRequest", true /* this is from a browser request*/)); + url = dataResource.getUri().toString(); + //Check if plugins intercept the request WebResourceResponse ret = super.shouldInterceptRequest(view, url); - if(ret == null && (url.contains("?") || url.contains("#") || needsIceCreamSpaceInAssetUrlFix(url))){ - ret = generateWebResourceResponse(url); +// The below bugfix is taken care of by the dataResource mechanism +// if(ret == null && (url.contains("?") || url.contains("#") || needsIceCreamSpaceInAssetUrlFix(url))){ +// ret = generateWebResourceResponse(url); +// } + if(ret == null) { + try { + ret = new WebResourceResponse(dataResource.getMimeType(), "UTF-8", dataResource.getIs()); + } catch(IOException e) { + LOG.e("IceCreamCordovaWebViewClient", "Error occurred while loading a file.", e); + } } return ret; } - - private WebResourceResponse generateWebResourceResponse(String url) { - if (url.startsWith("file:///android_asset/")) { - String mimetype = FileHelper.getMimeType(url, cordova); - - try { - InputStream stream = FileHelper.getInputStreamFromUriString(url, cordova); - WebResourceResponse response = new WebResourceResponse(mimetype, "UTF-8", stream); - return response; - } catch (IOException e) { - LOG.e("generateWebResourceResponse", e.getMessage(), e); - } - } - return null; - } - - private static boolean needsIceCreamSpaceInAssetUrlFix(String url) { - if (!url.contains("%20")){ - return false; - } - - switch(android.os.Build.VERSION.SDK_INT){ - case android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH: - case android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1: - return true; - default: - return false; - } - } - } diff --git a/framework/src/org/apache/cordova/api/CordovaPlugin.java b/framework/src/org/apache/cordova/api/CordovaPlugin.java index 2b225e64..69f8fdeb 100644 --- a/framework/src/org/apache/cordova/api/CordovaPlugin.java +++ b/framework/src/org/apache/cordova/api/CordovaPlugin.java @@ -175,6 +175,20 @@ public class CordovaPlugin { return null; } + /** + * All plugins can now choose if they want to modify any uri requests. This includes all webview requests, opening of files, content uri's etc. + * This mechanism allows several plugins to modify the same request + * @param requestSource The source of the incoming request + * + * @param dataResource The resource to be loaded. + * @param dataResourceContext Context associated with the resource load + * @return Return a new DataResource if the plugin wants o assist in loading the request or null if it doesn't. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public DataResource shouldInterceptDataResourceRequest(DataResource dataResource, DataResourceContext dataResourceContext) { + return null; + } + /** * Called when the WebView does a top-level navigation or refreshes. * diff --git a/framework/src/org/apache/cordova/api/DataResource.java b/framework/src/org/apache/cordova/api/DataResource.java new file mode 100644 index 00000000..8422b07d --- /dev/null +++ b/framework/src/org/apache/cordova/api/DataResource.java @@ -0,0 +1,138 @@ +package org.apache.cordova.api; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.cordova.FileHelper; + +import android.net.Uri; + +/* + * All requests to access files, browser network requests etc have to go through this class. + */ +public class DataResource { + private CordovaInterface cordova; + + // Uri of the request. Always required. + private Uri uri; + // Remaining fields may or may not be null + private InputStream is; + private OutputStream os; + private String mimeType; + private Boolean writable; + private File realFile; + private boolean retryLoad = true; + + public DataResource(CordovaInterface cordova, Uri uri) { + super(); + this.cordova = cordova; + this.uri = uri; + } + public DataResource(CordovaInterface cordova, Uri uri, InputStream is, + OutputStream os, String mimeType, boolean writable, File realFile) { + this(cordova, uri); + this.is = is; + this.mimeType = mimeType; + this.writable = Boolean.valueOf(writable); + this.realFile = realFile; + } + public Uri getUri() { + // Uri is always provided + return uri; + } + public InputStream getIs() throws IOException { + if(is == null && retryLoad) { + try { + is = FileHelper.getInputStreamFromUriString(uri.toString(), cordova); + } finally { + // We failed loading once, don't try loading anymore + if(is == null) { + retryLoad = false; + } + } + } + return is; + } + public OutputStream getOs() throws FileNotFoundException { + if(os == null && retryLoad) { + try { + os = FileHelper.getOutputStreamFromUriString(uri.toString(), cordova); + } finally { + // We failed loading once, don't try loading anymore + if(os == null) { + retryLoad = false; + } + } + } + return os; + } + public String getMimeType() { + if(mimeType == null && retryLoad) { + try { + mimeType = FileHelper.getMimeType(uri.toString(), cordova); + } finally { + // We failed loading once, don't try loading anymore + if(mimeType == null) { + retryLoad = false; + } + } + } + return mimeType; + } + public boolean isWritable() { + if(writable == null && retryLoad) { + try { + writable = FileHelper.isUriWritable(uri.toString()); + } finally { + // We failed loading once, don't try loading anymore + if(writable == null) { + retryLoad = false; + } + } + } + // default to false + return writable != null? writable.booleanValue() : false; + } + public File getRealFile() { + if(realFile == null && retryLoad) { + try { + String realPath = FileHelper.getRealPath(uri, cordova); + if(realPath != null) { + realFile = new File(realPath); + } + } finally { + // We failed loading once, don't try loading anymore + if(realFile == null) { + retryLoad = false; + } + } + } + return realFile; + } + + // static instantiation methods + public static DataResource initiateNewDataRequestForUri(String uriString, PluginManager pluginManager, CordovaInterface cordova, String requestSourceTag){ + // if no protocol is specified, assume its file: + uriString = FileHelper.insertFileProtocol(uriString); + return initiateNewDataRequestForUri(Uri.parse(uriString), pluginManager, cordova, requestSourceTag); + } + public static DataResource initiateNewDataRequestForUri(Uri uri, PluginManager pluginManager, CordovaInterface cordova, String requestSourceTag){ + return initiateNewDataRequestForUri(uri, pluginManager, cordova, new DataResourceContext(requestSourceTag, false /* Assume, not a browser request by default */ )); + } + public static DataResource initiateNewDataRequestForUri(String uriString, PluginManager pluginManager, CordovaInterface cordova, DataResourceContext dataResourceContext){ + // if no protocol is specified, assume its file: + uriString = FileHelper.insertFileProtocol(uriString); + return initiateNewDataRequestForUri(Uri.parse(uriString), pluginManager, cordova, dataResourceContext); + } + public static DataResource initiateNewDataRequestForUri(Uri uri, PluginManager pluginManager, CordovaInterface cordova, DataResourceContext dataResourceContext){ + DataResource dataResource = new DataResource(cordova, uri); + if (pluginManager != null) { + // get the resource as returned by plugins + dataResource = pluginManager.shouldInterceptDataResourceRequest(dataResource, dataResourceContext); + } + return dataResource; + } +} diff --git a/framework/src/org/apache/cordova/api/DataResourceContext.java b/framework/src/org/apache/cordova/api/DataResourceContext.java new file mode 100644 index 00000000..3c668171 --- /dev/null +++ b/framework/src/org/apache/cordova/api/DataResourceContext.java @@ -0,0 +1,38 @@ +package org.apache.cordova.api; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +/* + * Some context information associated with a DataRequest. + */ +public class DataResourceContext { + // A random id that is unique for a particular request. + private int requestId; + // A tag associated with the source of this dataResourceContext + private String source; + // If needed, any data associated with core plugins can be a part of the context object + // This field indicates whether the request came from a browser network request + private boolean isFromBrowser; + // If needed, any data associated with non core plugins should store data in a Map so as to not clutter the context object + private Map dataMap; + public DataResourceContext(String source, boolean isFromBrowser) { + super(); + this.requestId = new Random().nextInt(); + this.source = source; + this.isFromBrowser = isFromBrowser; + this.dataMap = new HashMap(); + } + public int getRequestId() { + return requestId; + } + public String getSource() { + return source; + } + public boolean isFromBrowser() { + return isFromBrowser; + } + public Map getDataMap() { + return dataMap; + } +} diff --git a/framework/src/org/apache/cordova/api/PluginManager.java b/framework/src/org/apache/cordova/api/PluginManager.java index 36e286ee..6bad5ed3 100755 --- a/framework/src/org/apache/cordova/api/PluginManager.java +++ b/framework/src/org/apache/cordova/api/PluginManager.java @@ -400,4 +400,30 @@ public class PluginManager { LOG.e(TAG, "https://git-wip-us.apache.org/repos/asf?p=incubator-cordova-android.git;a=blob;f=framework/res/xml/plugins.xml"); LOG.e(TAG, "====================================================================================="); } + + /** + * Called when the any resource is going to be loaded - either from the webview, files or any other resource + * + * + * @param dataResource The resource request to be loaded. + * @param dataResourceContext The context of the dataResource request + * @return Return the resource request that will be loaded. The returned request may be modified or unchanged. + */ + public DataResource shouldInterceptDataResourceRequest(DataResource dataResource, DataResourceContext dataResourceContext){ + boolean requestModified = true; + while(requestModified) { + requestModified = false; + for (PluginEntry entry : this.entries.values()) { + if (entry.plugin != null) { + DataResource ret = entry.plugin.shouldInterceptDataResourceRequest(dataResource, dataResourceContext); + if(ret != null) { + dataResource = ret; + requestModified = true; + break; + } + } + } + } + return dataResource; + } }