You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ag...@apache.org on 2013/06/28 18:01:07 UTC

[47/50] [abbrv] android commit: [CB-3384] Rewrite of DataResource into UriResolver + UriResolvers

[CB-3384] Rewrite of DataResource into UriResolver + UriResolvers

Includes unit tests woot!

Note that this remove CordovaPlugin.shouldInterceptRequest(). Should be
fine since this method was introduced only a couple of releases ago, was
never documented, and afaict was only used by the Chrome Cordova plugins.


Project: http://git-wip-us.apache.org/repos/asf/cordova-android/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-android/commit/892ffc8c
Tree: http://git-wip-us.apache.org/repos/asf/cordova-android/tree/892ffc8c
Diff: http://git-wip-us.apache.org/repos/asf/cordova-android/diff/892ffc8c

Branch: refs/heads/2.9.x
Commit: 892ffc8ce45a555b74d1a8a40e4694b8855d753e
Parents: fbf7f1c
Author: Andrew Grieve <ag...@chromium.org>
Authored: Fri Jun 7 10:17:28 2013 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Thu Jun 27 21:55:28 2013 -0400

----------------------------------------------------------------------
 .../src/org/apache/cordova/CordovaWebView.java  |  34 +++
 .../src/org/apache/cordova/FileHelper.java      |  33 ++-
 .../cordova/IceCreamCordovaWebViewClient.java   |  49 ++--
 .../src/org/apache/cordova/UriResolver.java     |  65 +++++
 .../src/org/apache/cordova/UriResolvers.java    | 277 +++++++++++++++++++
 .../org/apache/cordova/api/CordovaPlugin.java   |  15 +-
 .../org/apache/cordova/api/PluginManager.java   |  33 +--
 .../apache/cordova/test/UriResolversTest.java   | 263 ++++++++++++++++++
 .../actions/CordovaWebViewTestActivity.java     |   2 +-
 9 files changed, 694 insertions(+), 77 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-android/blob/892ffc8c/framework/src/org/apache/cordova/CordovaWebView.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/CordovaWebView.java b/framework/src/org/apache/cordova/CordovaWebView.java
index 67775a1..3a8e48d 100755
--- a/framework/src/org/apache/cordova/CordovaWebView.java
+++ b/framework/src/org/apache/cordova/CordovaWebView.java
@@ -943,4 +943,38 @@ public class CordovaWebView extends WebView {
     public void storeResult(int requestCode, int resultCode, Intent intent) {
         mResult = new ActivityResult(requestCode, resultCode, intent);
     }
+    
+    /**
+     * Resolves the given URI, giving plugins a chance to re-route or customly handle the URI.
+     * A white-list rejection will be returned if the URI does not pass the white-list.
+     * @return Never returns null.
+     * @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
+     *     resolved before being passed into this function.
+     */
+    public UriResolver resolveUri(Uri uri) {
+        return resolveUri(uri, false);
+    }
+    
+    UriResolver resolveUri(Uri uri, boolean fromWebView) {
+        if (!uri.isAbsolute()) {
+            throw new IllegalArgumentException("Relative URIs are not yet supported by resolveUri.");
+        }
+        // Check the against the white-list before delegating to plugins.
+        if (("http".equals(uri.getScheme()) || "https".equals(uri.getScheme())) && !Config.isUrlWhiteListed(uri.toString()))
+        {
+            LOG.w(TAG, "resolveUri - URL is not in whitelist: " + uri);
+            return new UriResolvers.ErrorUriResolver(uri, "Whitelist rejection");
+        }
+
+        // Give plugins a chance to handle the request.
+        UriResolver resolver = pluginManager.resolveUri(uri);
+        if (resolver == null && !fromWebView) {
+            resolver = UriResolvers.forUri(uri, cordova.getActivity());
+            if (resolver == null) {
+                resolver = new UriResolvers.ErrorUriResolver(uri, "Unresolvable URI");
+            }
+        }
+
+        return resolver;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/892ffc8c/framework/src/org/apache/cordova/FileHelper.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/FileHelper.java b/framework/src/org/apache/cordova/FileHelper.java
index ebbdc8d..0b2ba1c 100644
--- a/framework/src/org/apache/cordova/FileHelper.java
+++ b/framework/src/org/apache/cordova/FileHelper.java
@@ -26,9 +26,12 @@ 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.io.OutputStream;
+import java.nio.charset.Charset;
 import java.util.Locale;
 
 public class FileHelper {
@@ -124,6 +127,20 @@ public class FileHelper {
         return uriString;
     }
 
+    public static String getMimeTypeForExtension(String path) {
+        String extension = path;
+        int lastDot = extension.lastIndexOf('.');
+        if (lastDot != -1) {
+            extension = extension.substring(lastDot + 1);
+        }
+        // Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
+        extension = extension.toLowerCase(Locale.getDefault());
+        if (extension.equals("3ga")) {
+            return "audio/3gpp";
+        }
+        return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+    }
+    
     /**
      * Returns the mime type of the data specified by the given URI string.
      *
@@ -137,19 +154,7 @@ public class FileHelper {
         if (uriString.startsWith("content://")) {
             mimeType = cordova.getActivity().getContentResolver().getType(uri);
         } else {
-            // MimeTypeMap.getFileExtensionFromUrl() fails when there are query parameters.
-            String extension = uri.getPath();
-            int lastDot = extension.lastIndexOf('.');
-            if (lastDot != -1) {
-                extension = extension.substring(lastDot + 1);
-            }
-            // Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
-            extension = extension.toLowerCase();
-            if (extension.equals("3ga")) {
-                mimeType = "audio/3gpp";
-            } else {
-                mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
-            }
+            mimeType = getMimeTypeForExtension(uri.getPath());
         }
 
         return mimeType;

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/892ffc8c/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java
index 1e190b6..8527d35 100644
--- a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java
+++ b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java
@@ -18,7 +18,6 @@
 */
 package org.apache.cordova;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 
@@ -26,6 +25,7 @@ import org.apache.cordova.api.CordovaInterface;
 import org.apache.cordova.api.LOG;
 
 import android.annotation.TargetApi;
+import android.net.Uri;
 import android.os.Build;
 import android.webkit.WebResourceResponse;
 import android.webkit.WebView;
@@ -44,45 +44,29 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient {
 
     @Override
     public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
-        //Check if plugins intercept the request
-        WebResourceResponse ret = super.shouldInterceptRequest(view, url);
+        UriResolver uriResolver = appView.resolveUri(Uri.parse(url), true);
         
-        if(!Config.isUrlWhiteListed(url) && (url.startsWith("http://") || url.startsWith("https://")))
-        {
-            ret =  getWhitelistResponse();
-        }
-        else if(ret == null && (url.contains("?") || url.contains("#") || needsIceCreamSpecialsInAssetUrlFix(url))){
-            ret = generateWebResourceResponse(url);
-        }
-        else if (ret == null && this.appView.pluginManager != null) {
-            ret = this.appView.pluginManager.shouldInterceptRequest(url);
+        if (uriResolver == null && url.startsWith("file:///android_asset/")) {
+            if (url.contains("?") || url.contains("#") || needsIceCreamSpecialsInAssetUrlFix(url)) {
+                uriResolver = appView.resolveUri(Uri.parse(url), false);
+            }
         }
-        return ret;
-    }
-    
-    private WebResourceResponse getWhitelistResponse()
-    {
-        WebResourceResponse emptyResponse;
-        String empty = "";
-        ByteArrayInputStream data = new ByteArrayInputStream(empty.getBytes());
-        return new WebResourceResponse("text/plain", "UTF-8", data);
-    }
-
-    private WebResourceResponse generateWebResourceResponse(String url) {
-        if (url.startsWith("file:///android_asset/")) {
-            String mimetype = FileHelper.getMimeType(url, cordova);
-
+        
+        if (uriResolver != null) {
             try {
-                InputStream stream = FileHelper.getInputStreamFromUriString(url, cordova);
-                WebResourceResponse response = new WebResourceResponse(mimetype, "UTF-8", stream);
-                return response;
+                InputStream stream = uriResolver.getInputStream();
+                String mimeType = uriResolver.getMimeType();
+                // If we don't know how to open this file, let the browser continue loading
+                return new WebResourceResponse(mimeType, "UTF-8", stream);
             } catch (IOException e) {
-                LOG.e("generateWebResourceResponse", e.getMessage(), e);
+                LOG.e("IceCreamCordovaWebViewClient", "Error occurred while loading a file.", e);
+                // Results in a 404.
+                return new WebResourceResponse("text/plain", "UTF-8", null);
             }
         }
         return null;
     }
-
+        
     private static boolean needsIceCreamSpecialsInAssetUrlFix(String url) {
         if (!url.contains("%20")){
             return false;
@@ -96,5 +80,4 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient {
                 return false;
         }
     }
-    
 }

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/892ffc8c/framework/src/org/apache/cordova/UriResolver.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/UriResolver.java b/framework/src/org/apache/cordova/UriResolver.java
new file mode 100644
index 0000000..42e9a3a
--- /dev/null
+++ b/framework/src/org/apache/cordova/UriResolver.java
@@ -0,0 +1,65 @@
+/*
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+*/
+package org.apache.cordova;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import android.net.Uri;
+
+/*
+ * Interface for a class that can resolve URIs.
+ * See CordovaUriResolver for an example.
+ */
+public interface UriResolver {
+
+    /** Returns the URI that this instance will resolve. */
+    Uri getUri();
+    
+    /** 
+     * Returns the InputStream for the resource. 
+     * Throws an exception if it cannot be read. 
+     * Never returns null.
+     */
+    InputStream getInputStream() throws IOException;
+
+    /** 
+     * Returns the OutputStream for the resource. 
+     * Throws an exception if it cannot be written to. 
+     * Never returns null.
+     */
+    OutputStream getOutputStream() throws IOException; 
+    
+    /** 
+     * Returns the MIME type of the resource.
+     * Returns null if the MIME type cannot be determined (e.g. content: that doesn't exist).
+     */
+    String getMimeType();
+
+    /** Returns whether the resource is writable. */
+    boolean isWritable();
+
+    /**
+     * Returns a File that points to the resource, or null if the resource
+     * is not on the local file system.
+     */
+    File getLocalFile();
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/892ffc8c/framework/src/org/apache/cordova/UriResolvers.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/UriResolvers.java b/framework/src/org/apache/cordova/UriResolvers.java
new file mode 100644
index 0000000..e8be407
--- /dev/null
+++ b/framework/src/org/apache/cordova/UriResolvers.java
@@ -0,0 +1,277 @@
+/*
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+*/
+package org.apache.cordova;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.cordova.FileHelper;
+import org.apache.http.util.EncodingUtils;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.net.Uri;
+
+/*
+ * UriResolver implementations.
+ */
+public final class UriResolvers {
+    private UriResolvers() {}
+
+    private static final class FileUriResolver implements UriResolver {
+        private final Uri uri;
+        private String mimeType;
+        private File localFile;
+    
+        FileUriResolver(Uri uri) {
+            this.uri = uri;
+        }
+        
+        public Uri getUri() {
+            return uri;
+        }
+        
+        public InputStream getInputStream() throws IOException {
+            return new FileInputStream(getLocalFile());
+        }
+        
+        public OutputStream getOutputStream() throws FileNotFoundException {
+            return new FileOutputStream(getLocalFile());
+        }
+        
+        public String getMimeType() {
+            if (mimeType == null) {
+                mimeType = FileHelper.getMimeTypeForExtension(getLocalFile().getName());
+            }
+            return mimeType;
+        }
+        
+        public boolean isWritable() {
+            File f = getLocalFile();
+            if (f.isDirectory()) {
+                return false;
+            }
+            if (f.exists()) {
+                return f.canWrite();
+            }
+            return f.getParentFile().canWrite();
+        }
+        
+        public File getLocalFile() {
+            if (localFile == null) {
+                localFile = new File(uri.getPath());
+            }
+            return localFile;
+        }
+    }
+    
+    private static final class AssetUriResolver implements UriResolver {
+        private final Uri uri;
+        private final AssetManager assetManager;
+        private final String assetPath;
+        private String mimeType;
+    
+        AssetUriResolver(Uri uri, AssetManager assetManager) {
+            this.uri = uri;
+            this.assetManager = assetManager;
+            this.assetPath = uri.getPath().substring(15);
+        }
+        
+        public Uri getUri() {
+            return uri;
+        }
+        
+        public InputStream getInputStream() throws IOException {
+            return assetManager.open(assetPath);
+        }
+        
+        public OutputStream getOutputStream() throws FileNotFoundException {
+            throw new FileNotFoundException("URI not writable.");
+        }
+        
+        public String getMimeType() {
+            if (mimeType == null) {
+                mimeType = FileHelper.getMimeTypeForExtension(assetPath);
+            }
+            return mimeType;
+        }
+        
+        public boolean isWritable() {
+            return false;
+        }
+        
+        public File getLocalFile() {
+            return null;
+        }
+    }
+    
+    private static final class ContentUriResolver implements UriResolver {
+        private final Uri uri;
+        private final ContentResolver contentResolver;
+        private String mimeType;
+    
+        ContentUriResolver(Uri uri, ContentResolver contentResolver) {
+            this.uri = uri;
+            this.contentResolver = contentResolver;
+        }
+        
+        public Uri getUri() {
+            return uri;
+        }
+        
+        public InputStream getInputStream() throws IOException {
+            return contentResolver.openInputStream(uri);
+        }
+        
+        public OutputStream getOutputStream() throws FileNotFoundException {
+            return contentResolver.openOutputStream(uri);
+        }
+        
+        public String getMimeType() {
+            if (mimeType == null) {
+                mimeType = contentResolver.getType(uri);
+            }
+            return mimeType;
+        }
+        
+        public boolean isWritable() {
+            return uri.getScheme().equals(ContentResolver.SCHEME_CONTENT);
+        }
+        
+        public File getLocalFile() {
+            return null;
+        }
+    }
+    
+    static final class ErrorUriResolver implements UriResolver {
+        final Uri uri;
+        final String errorMsg;
+        
+        ErrorUriResolver(Uri uri, String errorMsg) {
+            this.uri = uri;
+            this.errorMsg = errorMsg;
+        }
+        
+        @Override
+        public boolean isWritable() {
+            return false;
+        }
+        
+        @Override
+        public Uri getUri() {
+            return uri;
+        }
+        
+        @Override
+        public File getLocalFile() {
+            return null;
+        }
+        
+        @Override
+        public OutputStream getOutputStream() throws IOException {
+            throw new FileNotFoundException(errorMsg);
+        }
+        
+        @Override
+        public String getMimeType() {
+            return null;
+        }
+        
+        @Override
+        public InputStream getInputStream() throws IOException {
+            throw new FileNotFoundException(errorMsg);
+        }
+    }
+    
+    private static final class ReadOnlyResolver implements UriResolver {
+        private Uri uri;
+        private InputStream inputStream;
+        private String mimeType;
+        
+        public ReadOnlyResolver(Uri uri, InputStream inputStream, String mimeType) {
+            this.uri = uri;
+            this.inputStream = inputStream;
+            this.mimeType = mimeType;
+        }
+        
+        @Override
+        public boolean isWritable() {
+            return false;
+        }
+        
+        @Override
+        public Uri getUri() {
+            return uri;
+        }
+        
+        @Override
+        public File getLocalFile() {
+            return null;
+        }
+        
+        @Override
+        public OutputStream getOutputStream() throws IOException {
+            throw new FileNotFoundException("URI is not writable");
+        }
+        
+        @Override
+        public String getMimeType() {
+            return mimeType;
+        }
+        
+        @Override
+        public InputStream getInputStream() throws IOException {
+            return inputStream;
+        }
+    }
+    
+    public static UriResolver createInline(Uri uri, String response, String mimeType) {
+        return createInline(uri, EncodingUtils.getBytes(response, "UTF-8"), mimeType);
+    }
+    
+    public static UriResolver createInline(Uri uri, byte[] response, String mimeType) {
+        return new ReadOnlyResolver(uri, new ByteArrayInputStream(response), mimeType);
+    }
+
+    public static UriResolver createReadOnly(Uri uri, InputStream inputStream, String mimeType) {
+        return new ReadOnlyResolver(uri, inputStream, mimeType);
+    }
+    
+    /* Package-private to force clients to go through CordovaWebView.resolveUri(). */
+    static UriResolver forUri(Uri uri, Context context) {
+        String scheme = uri.getScheme();
+        if (ContentResolver.SCHEME_CONTENT.equals(scheme) || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+            return new ContentUriResolver(uri, context.getContentResolver());
+        }
+        if (ContentResolver.SCHEME_FILE.equals(scheme)) {
+            if (uri.getPath().startsWith("/android_asset/")) {
+                return new AssetUriResolver(uri, context.getAssets());
+            }
+            return new FileUriResolver(uri);
+        }
+        return null;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/892ffc8c/framework/src/org/apache/cordova/api/CordovaPlugin.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/api/CordovaPlugin.java b/framework/src/org/apache/cordova/api/CordovaPlugin.java
index 2b225e6..07035e5 100644
--- a/framework/src/org/apache/cordova/api/CordovaPlugin.java
+++ b/framework/src/org/apache/cordova/api/CordovaPlugin.java
@@ -20,14 +20,12 @@ package org.apache.cordova.api;
 
 import org.apache.cordova.CordovaArgs;
 import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.UriResolver;
 import org.json.JSONArray;
 import org.json.JSONException;
 
-import android.annotation.TargetApi;
 import android.content.Intent;
-import android.os.Build;
-import android.util.Log;
-import android.webkit.WebResourceResponse;
+import android.net.Uri;
 
 /**
  * Plugins must extend this class and override one of the execute methods.
@@ -165,13 +163,10 @@ public class CordovaPlugin {
     }
 
     /**
-     * By specifying a <url-filter> in config.xml you can map a URL prefix to this method. It applies to all resources loaded in the WebView, not just top-level navigation.
-     *
-     * @param url               The URL of the resource to be loaded.
-     * @return                  Return a WebResourceResponse for the resource, or null to let the WebView handle it normally.
+     * Hook for overriding the default URI handling mechanism.
+     * Applies to WebView requests as well as requests made by plugins.
      */
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
-	public WebResourceResponse shouldInterceptRequest(String url) {
+    public UriResolver resolveUri(Uri uri) {
         return null;
     }
 

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/892ffc8c/framework/src/org/apache/cordova/api/PluginManager.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/api/PluginManager.java b/framework/src/org/apache/cordova/api/PluginManager.java
index e392dfa..083e882 100755
--- a/framework/src/org/apache/cordova/api/PluginManager.java
+++ b/framework/src/org/apache/cordova/api/PluginManager.java
@@ -26,12 +26,14 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.cordova.CordovaArgs;
 import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.UriResolver;
 import org.json.JSONException;
 import org.xmlpull.v1.XmlPullParserException;
 
 import android.content.Intent;
 import android.content.res.XmlResourceParser;
 
+import android.net.Uri;
 import android.util.Log;
 import android.webkit.WebResourceResponse;
 
@@ -380,25 +382,6 @@ public class PluginManager {
     }
 
     /**
-     * Called when the WebView is loading any resource, top-level or not.
-     *
-     * Uses the same url-filter tag as onOverrideUrlLoading.
-     *
-     * @param url               The URL of the resource to be loaded.
-     * @return                  Return a WebResourceResponse with the resource, or null if the WebView should handle it.
-     */
-    public WebResourceResponse shouldInterceptRequest(String url) {
-        Iterator<Entry<String, String>> it = this.urlMap.entrySet().iterator();
-        while (it.hasNext()) {
-            HashMap.Entry<String, String> pairs = it.next();
-            if (url.startsWith(pairs.getKey())) {
-                return this.getPlugin(pairs.getValue()).shouldInterceptRequest(url);
-            }
-        }
-        return null;
-    }
-
-    /**
      * Called when the app navigates or refreshes.
      */
     public void onReset() {
@@ -419,6 +402,18 @@ public class PluginManager {
         LOG.e(TAG, "=====================================================================================");
     }
 
+    /* Should be package private */ public UriResolver resolveUri(Uri uri) {
+        for (PluginEntry entry : this.entries.values()) {
+            if (entry.plugin != null) {
+                UriResolver ret = entry.plugin.resolveUri(uri);
+                if (ret != null) {
+                    return ret;
+                }
+            }
+        }
+        return null;
+    }
+
     private class PluginManagerService extends CordovaPlugin {
         @Override
         public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException {

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/892ffc8c/test/src/org/apache/cordova/test/UriResolversTest.java
----------------------------------------------------------------------
diff --git a/test/src/org/apache/cordova/test/UriResolversTest.java b/test/src/org/apache/cordova/test/UriResolversTest.java
new file mode 100644
index 0000000..21584b9
--- /dev/null
+++ b/test/src/org/apache/cordova/test/UriResolversTest.java
@@ -0,0 +1,263 @@
+
+package org.apache.cordova.test;
+
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.UriResolver;
+import org.apache.cordova.UriResolvers;
+import org.apache.cordova.api.CallbackContext;
+import org.apache.cordova.api.CordovaPlugin;
+import org.apache.cordova.api.PluginEntry;
+import org.apache.cordova.test.actions.CordovaWebViewTestActivity;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.File;
+import java.io.IOException;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+public class UriResolversTest extends ActivityInstrumentationTestCase2<CordovaWebViewTestActivity> {
+
+    public UriResolversTest()
+    {
+        super(CordovaWebViewTestActivity.class);
+    }
+
+    CordovaWebView cordovaWebView;
+    private CordovaWebViewTestActivity activity;
+    String execPayload;
+    Integer execStatus;
+
+    protected void setUp() throws Exception {
+        super.setUp();
+        activity = this.getActivity();
+        cordovaWebView = activity.cordovaWebView;
+        cordovaWebView.pluginManager.addService(new PluginEntry("UriResolverTestPlugin1", new CordovaPlugin() {
+            @Override
+            public UriResolver resolveUri(Uri uri) {
+                if ("plugin-uri".equals(uri.getScheme())) {
+                    return cordovaWebView.resolveUri(uri.buildUpon().scheme("file").build());
+                }
+                return null;
+            }
+            public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+                synchronized (UriResolversTest.this) {
+                    execPayload = args.getString(0);
+                    execStatus = args.getInt(1);
+                    UriResolversTest.this.notify();
+                }
+                return true;
+            }
+        }));
+        cordovaWebView.pluginManager.addService(new PluginEntry("UriResolverTestPlugin2", new CordovaPlugin() {
+            @Override
+            public UriResolver resolveUri(Uri uri) {
+                if (uri.getQueryParameter("pluginRewrite") != null) {
+                    return UriResolvers.createInline(uri, "pass", "my/mime");
+                }
+                return null;
+            }
+        }));
+    }
+
+    private Uri createTestImageContentUri() {
+        Bitmap imageBitmap = BitmapFactory.decodeResource(activity.getResources(), R.drawable.icon);
+        String stored = MediaStore.Images.Media.insertImage(activity.getContentResolver(),
+                imageBitmap, "app-icon", "desc");
+        return Uri.parse(stored);
+    }
+
+    private void performResolverTest(Uri uri, String expectedMimeType, File expectedLocalFile,
+            boolean expectedIsWritable,
+            boolean expectRead, boolean expectWrite) throws IOException {
+        UriResolver resolver = cordovaWebView.resolveUri(uri);
+        assertEquals(expectedLocalFile, resolver.getLocalFile());
+        assertEquals(expectedMimeType, resolver.getMimeType());
+        if (expectedIsWritable) {
+            assertTrue(resolver.isWritable());
+        } else {
+            assertFalse(resolver.isWritable());
+        }
+        try {
+            resolver.getInputStream().read();
+            if (!expectRead) {
+                fail("Expected getInputStream to throw.");
+            }
+        } catch (IOException e) {
+            if (expectRead) {
+                throw e;
+            }
+        }
+        try {
+            resolver.getOutputStream().write(123);
+            if (!expectWrite) {
+                fail("Expected getOutputStream to throw.");
+            }
+        } catch (IOException e) {
+            if (expectWrite) {
+                throw e;
+            }
+        }
+    }
+
+    public void testValidContentUri() throws IOException
+    {
+        Uri contentUri = createTestImageContentUri();
+        performResolverTest(contentUri, "image/jpeg", null, true, true, true);
+    }
+
+    public void testInvalidContentUri() throws IOException
+    {
+        Uri contentUri = Uri.parse("content://media/external/images/media/999999999");
+        performResolverTest(contentUri, null, null, true, false, false);
+    }
+
+    public void testValidAssetUri() throws IOException
+    {
+        Uri assetUri = Uri.parse("file:///android_asset/www/index.html?foo#bar"); // Also check for stripping off ? and # correctly.
+        performResolverTest(assetUri, "text/html", null, false, true, false);
+    }
+
+    public void testInvalidAssetUri() throws IOException
+    {
+        Uri assetUri = Uri.parse("file:///android_asset/www/missing.html");
+        performResolverTest(assetUri, "text/html", null, false, false, false);
+    }
+
+    public void testFileUriToExistingFile() throws IOException
+    {
+        File f = File.createTempFile("te s t", ".txt"); // Also check for dealing with spaces.
+        try {
+            Uri fileUri = Uri.parse(f.toURI().toString() + "?foo#bar"); // Also check for stripping off ? and # correctly.
+            performResolverTest(fileUri, "text/plain", f, true, true, true);
+        } finally {
+            f.delete();
+        }
+    }
+
+    public void testFileUriToMissingFile() throws IOException
+    {
+        File f = new File(Environment.getExternalStorageDirectory() + "/somefilethatdoesntexist");
+        Uri fileUri = Uri.parse(f.toURI().toString());
+        try {
+            performResolverTest(fileUri, null, f, true, false, true);
+        } finally {
+            f.delete();
+        }
+    }
+    
+    public void testFileUriToMissingFileWithMissingParent() throws IOException
+    {
+        File f = new File(Environment.getExternalStorageDirectory() + "/somedirthatismissing/somefilethatdoesntexist");
+        Uri fileUri = Uri.parse(f.toURI().toString());
+        performResolverTest(fileUri, null, f, false, false, false);
+    }
+
+    public void testUnrecognizedUri() throws IOException
+    {
+        Uri uri = Uri.parse("somescheme://foo");
+        performResolverTest(uri, null, null, false, false, false);
+    }
+
+    public void testRelativeUri()
+    {
+        try {
+            cordovaWebView.resolveUri(Uri.parse("/foo"));
+            fail("Should have thrown for relative URI 1.");
+        } catch (Throwable t) {
+        }
+        try {
+            cordovaWebView.resolveUri(Uri.parse("//foo/bar"));
+            fail("Should have thrown for relative URI 2.");
+        } catch (Throwable t) {
+        }
+        try {
+            cordovaWebView.resolveUri(Uri.parse("foo.png"));
+            fail("Should have thrown for relative URI 3.");
+        } catch (Throwable t) {
+        }
+    }
+    
+    public void testPluginOverrides1() throws IOException
+    {
+        Uri uri = Uri.parse("plugin-uri://foohost/android_asset/www/index.html");
+        performResolverTest(uri, "text/html", null, false, true, false);
+    }
+
+    public void testPluginOverrides2() throws IOException
+    {
+        Uri uri = Uri.parse("plugin-uri://foohost/android_asset/www/index.html?pluginRewrite=yes");
+        performResolverTest(uri, "my/mime", null, false, true, false);
+    }
+
+    public void testWhitelistRejection() throws IOException
+    {
+        Uri uri = Uri.parse("http://foohost.com/");
+        performResolverTest(uri, null, null, false, false, false);
+    }
+    
+    public void testWebViewRequestIntercept() throws IOException
+    {
+        cordovaWebView.sendJavascript(
+            "var x = new XMLHttpRequest;\n" +
+            "x.open('GET', 'file://foo?pluginRewrite=1', false);\n" + 
+            "x.send();\n" + 
+            "cordova.require('cordova/exec')(null,null,'UriResolverTestPlugin1', 'foo', [x.responseText, x.status])");
+        execPayload = null;
+        execStatus = null;
+        try {
+            synchronized (this) {
+                this.wait(2000);
+            }
+        } catch (InterruptedException e) {
+        }
+        assertEquals("pass", execPayload);
+        assertEquals(execStatus.intValue(), 200);
+    }
+    
+    public void testWebViewWhiteListRejection() throws IOException
+    {
+        cordovaWebView.sendJavascript(
+            "var x = new XMLHttpRequest;\n" +
+            "x.open('GET', 'http://foo/bar', false);\n" + 
+            "x.send();\n" + 
+            "cordova.require('cordova/exec')(null,null,'UriResolverTestPlugin1', 'foo', [x.responseText, x.status])");
+        execPayload = null;
+        execStatus = null;
+        try {
+            synchronized (this) {
+                this.wait(2000);
+            }
+        } catch (InterruptedException e) {
+        }
+        assertEquals("", execPayload);
+        assertEquals(execStatus.intValue(), 404);
+    }    
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/892ffc8c/test/src/org/apache/cordova/test/actions/CordovaWebViewTestActivity.java
----------------------------------------------------------------------
diff --git a/test/src/org/apache/cordova/test/actions/CordovaWebViewTestActivity.java b/test/src/org/apache/cordova/test/actions/CordovaWebViewTestActivity.java
index 1185bad..3be5ab2 100644
--- a/test/src/org/apache/cordova/test/actions/CordovaWebViewTestActivity.java
+++ b/test/src/org/apache/cordova/test/actions/CordovaWebViewTestActivity.java
@@ -36,7 +36,7 @@ import android.content.Intent;
 import android.os.Bundle;
 
 public class CordovaWebViewTestActivity extends Activity implements CordovaInterface {
-    CordovaWebView cordovaWebView;
+    public CordovaWebView cordovaWebView;
 
     private final ExecutorService threadPool = Executors.newCachedThreadPool();