From 77e9092108b4997abdc08afa630b8c16ee094f90 Mon Sep 17 00:00:00 2001 From: Andrew Grieve Date: Sun, 14 Jul 2013 21:39:55 -0400 Subject: [PATCH] [CB-3384] Reworked UriResolver into CordovaResourceApi. Changes were made after trying to use the API for Camera, FileTransfer, Media. The main difference is separating the concept of URI remapping from the read/write helpers. --- .../src/org/apache/cordova/CordovaPlugin.java | 8 +- .../apache/cordova/CordovaResourceApi.java | 347 ++++++++++++++++++ .../org/apache/cordova/CordovaWebView.java | 39 +- .../apache/cordova/CordovaWebViewClient.java | 2 +- .../cordova/IceCreamCordovaWebViewClient.java | 61 +-- .../src/org/apache/cordova/PluginManager.java | 6 +- .../src/org/apache/cordova/UriResolver.java | 69 ---- .../src/org/apache/cordova/UriResolvers.java | 341 ----------------- test/AndroidManifest.xml | 2 +- ...sTest.java => CordovaResourceApiTest.java} | 130 ++++--- 10 files changed, 466 insertions(+), 539 deletions(-) create mode 100644 framework/src/org/apache/cordova/CordovaResourceApi.java delete mode 100644 framework/src/org/apache/cordova/UriResolver.java delete mode 100644 framework/src/org/apache/cordova/UriResolvers.java rename test/src/org/apache/cordova/test/{UriResolversTest.java => CordovaResourceApiTest.java} (62%) diff --git a/framework/src/org/apache/cordova/CordovaPlugin.java b/framework/src/org/apache/cordova/CordovaPlugin.java index 456633b3..455f56f3 100644 --- a/framework/src/org/apache/cordova/CordovaPlugin.java +++ b/framework/src/org/apache/cordova/CordovaPlugin.java @@ -22,7 +22,6 @@ import org.apache.cordova.CordovaArgs; import org.apache.cordova.CordovaWebView; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CallbackContext; -import org.apache.cordova.UriResolver; import org.json.JSONArray; import org.json.JSONException; @@ -165,13 +164,12 @@ public class CordovaPlugin { } /** - * Hook for overriding the default URI handling mechanism. - * Applies to WebView requests as well as requests made by plugins. + * Hook for redirecting requests. Applies to WebView requests as well as requests made by plugins. */ - public UriResolver resolveUri(Uri uri) { + public Uri remapUri(Uri uri) { return null; } - + /** * Called when the WebView does a top-level navigation or refreshes. * diff --git a/framework/src/org/apache/cordova/CordovaResourceApi.java b/framework/src/org/apache/cordova/CordovaResourceApi.java new file mode 100644 index 00000000..b891b51a --- /dev/null +++ b/framework/src/org/apache/cordova/CordovaResourceApi.java @@ -0,0 +1,347 @@ +/* + 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 android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Looper; +import android.util.Base64; +import android.util.Base64InputStream; + +import com.squareup.okhttp.OkHttpClient; + +import org.apache.http.util.EncodingUtils; + +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 java.net.HttpURLConnection; +import java.net.URL; +import java.nio.channels.FileChannel; + +public class CordovaResourceApi { + @SuppressWarnings("unused") + private static final String LOG_TAG = "CordovaResourceApi"; + + public static final int URI_TYPE_FILE = 0; + public static final int URI_TYPE_ASSET = 1; + public static final int URI_TYPE_CONTENT = 2; + public static final int URI_TYPE_RESOURCE = 3; + public static final int URI_TYPE_DATA = 4; + public static final int URI_TYPE_HTTP = 5; + public static final int URI_TYPE_HTTPS = 6; + public static final int URI_TYPE_UNKNOWN = -1; + + private static final String[] LOCAL_FILE_PROJECTION = { "_data" }; + + // Creating this is light-weight. + private static OkHttpClient httpClient = new OkHttpClient(); + + static Thread webCoreThread; + + private final AssetManager assetManager; + private final ContentResolver contentResolver; + private final PluginManager pluginManager; + private boolean threadCheckingEnabled = true; + + + public CordovaResourceApi(Context context, PluginManager pluginManager) { + this.contentResolver = context.getContentResolver(); + this.assetManager = context.getAssets(); + this.pluginManager = pluginManager; + } + + public void setThreadCheckingEnabled(boolean value) { + threadCheckingEnabled = value; + } + + public boolean isThreadCheckingEnabled() { + return threadCheckingEnabled; + } + + public static int getUriType(Uri uri) { + assertNonRelative(uri); + String scheme = uri.getScheme(); + if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { + return URI_TYPE_CONTENT; + } + if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { + return URI_TYPE_RESOURCE; + } + if (ContentResolver.SCHEME_FILE.equals(scheme)) { + if (uri.getPath().startsWith("/android_asset/")) { + return URI_TYPE_ASSET; + } + return URI_TYPE_FILE; + } + if ("data".equals(scheme)) { + return URI_TYPE_DATA; + } + if ("http".equals(scheme)) { + return URI_TYPE_HTTP; + } + if ("https".equals(scheme)) { + return URI_TYPE_HTTPS; + } + return URI_TYPE_UNKNOWN; + } + + public Uri remapUri(Uri uri) { + assertNonRelative(uri); + Uri pluginUri = pluginManager.remapUri(uri); + return pluginUri != null ? pluginUri : uri; + } + + public String remapPath(String path) { + return remapUri(Uri.fromFile(new File(path))).getPath(); + } + + /** + * Returns a File that points to the resource, or null if the resource + * is not on the local filesystem. + */ + public File mapUriToFile(Uri uri) { + assertBackgroundThread(); + switch (getUriType(uri)) { + case URI_TYPE_FILE: + return new File(uri.getPath()); + case URI_TYPE_CONTENT: { + Cursor cursor = contentResolver.query(uri, LOCAL_FILE_PROJECTION, null, null, null); + if (cursor != null) { + try { + int columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]); + if (columnIndex != -1 && cursor.getCount() > 0) { + cursor.moveToFirst(); + String realPath = cursor.getString(columnIndex); + if (realPath != null) { + return new File(realPath); + } + } + } finally { + cursor.close(); + } + } + } + } + return null; + } + + /** + * Opens a stream to the givne URI, also providing the MIME type & length. + * @return Never returns null. + * @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be + * resolved before being passed into this function. + * @throws Throws an IOException if the URI cannot be opened. + */ + public OpenForReadResult openForRead(Uri uri) throws IOException { + assertBackgroundThread(); + switch (getUriType(uri)) { + case URI_TYPE_FILE: { + FileInputStream inputStream = new FileInputStream(uri.getPath()); + String mimeType = FileHelper.getMimeTypeForExtension(uri.getPath()); + long length = inputStream.getChannel().size(); + return new OpenForReadResult(uri, inputStream, mimeType, length, null); + } + case URI_TYPE_ASSET: { + String assetPath = uri.getPath().substring(15); + AssetFileDescriptor assetFd = null; + InputStream inputStream; + long length = -1; + try { + assetFd = assetManager.openFd(assetPath); + inputStream = assetFd.createInputStream(); + length = assetFd.getLength(); + } catch (FileNotFoundException e) { + // Will occur if the file is compressed. + inputStream = assetManager.open(assetPath); + } + String mimeType = FileHelper.getMimeTypeForExtension(assetPath); + return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd); + } + case URI_TYPE_CONTENT: + case URI_TYPE_RESOURCE: { + String mimeType = contentResolver.getType(uri); + AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, "r"); + InputStream inputStream = assetFd.createInputStream(); + long length = assetFd.getLength(); + return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd); + } + case URI_TYPE_DATA: { + OpenForReadResult ret = readDataUri(uri); + if (ret == null) { + break; + } + return ret; + } + case URI_TYPE_HTTP: + case URI_TYPE_HTTPS: { + HttpURLConnection conn = httpClient.open(new URL(uri.toString())); + conn.setDoInput(true); + String mimeType = conn.getHeaderField("Content-Type"); + int length = conn.getContentLength(); + InputStream inputStream = conn.getInputStream(); + return new OpenForReadResult(uri, inputStream, mimeType, length, null); + } + } + throw new FileNotFoundException("URI not supported by CordovaResourceApi: " + uri); + } + + public OutputStream openOutputStream(Uri uri) throws IOException { + return openOutputStream(uri, false); + } + + /** + * Opens a stream to the given URI. + * @return Never returns null. + * @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be + * resolved before being passed into this function. + * @throws Throws an IOException if the URI cannot be opened. + */ + public OutputStream openOutputStream(Uri uri, boolean append) throws IOException { + assertBackgroundThread(); + switch (getUriType(uri)) { + case URI_TYPE_FILE: { + File localFile = new File(uri.getPath()); + File parent = localFile.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + return new FileOutputStream(localFile, append); + } + case URI_TYPE_CONTENT: + case URI_TYPE_RESOURCE: { + AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, append ? "wa" : "w"); + return assetFd.createOutputStream(); + } + } + throw new FileNotFoundException("URI not supported by CordovaResourceApi: " + uri); + } + + public HttpURLConnection createHttpConnection(Uri uri) throws IOException { + assertBackgroundThread(); + return httpClient.open(new URL(uri.toString())); + } + + // Copies the input to the output in the most efficient manner possible. + // Closes both streams. + public void copyResource(OpenForReadResult input, OutputStream outputStream) throws IOException { + assertBackgroundThread(); + try { + InputStream inputStream = input.inputStream; + if (inputStream instanceof FileInputStream && outputStream instanceof FileOutputStream) { + FileChannel inChannel = ((FileInputStream)input.inputStream).getChannel(); + FileChannel outChannel = ((FileOutputStream)outputStream).getChannel(); + long offset = 0; + long length = input.length; + if (input.assetFd != null) { + offset = input.assetFd.getStartOffset(); + } + outChannel.transferFrom(inChannel, offset, length); + } else { + final int BUFFER_SIZE = 8192; + byte[] buffer = new byte[BUFFER_SIZE]; + + for (;;) { + int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE); + + if (bytesRead <= 0) { + break; + } + outputStream.write(buffer, 0, bytesRead); + } + } + } finally { + input.inputStream.close(); + if (outputStream != null) { + outputStream.close(); + } + } + } + + public void copyResource(Uri sourceUri, OutputStream outputStream) throws IOException { + copyResource(openForRead(sourceUri), outputStream); + } + + + private void assertBackgroundThread() { + if (threadCheckingEnabled) { + Thread curThread = Thread.currentThread(); + if (curThread == Looper.getMainLooper().getThread()) { + throw new IllegalStateException("Do not perform IO operations on the UI thread. Use CordovaInterface.getThreadPool() instead."); + } + if (curThread == webCoreThread) { + throw new IllegalStateException("Tried to perform an IO operation on the WebCore thread. Use CordovaInterface.getThreadPool() instead."); + } + } + } + + private OpenForReadResult readDataUri(Uri uri) { + String uriAsString = uri.toString().substring(5); + int commaPos = uriAsString.indexOf(','); + if (commaPos == -1) { + return null; + } + String[] mimeParts = uriAsString.substring(0, commaPos).split(";"); + String contentType = null; + boolean base64 = false; + if (mimeParts.length > 0) { + contentType = mimeParts[0]; + } + for (int i = 1; i < mimeParts.length; ++i) { + if ("base64".equalsIgnoreCase(mimeParts[i])) { + base64 = true; + } + } + String dataPartAsString = uriAsString.substring(commaPos + 1); + byte[] data = base64 ? Base64.decode(dataPartAsString, Base64.DEFAULT) : EncodingUtils.getBytes(dataPartAsString, "UTF-8"); + InputStream inputStream = new ByteArrayInputStream(data); + return new OpenForReadResult(uri, inputStream, contentType, data.length, null); + } + + private static void assertNonRelative(Uri uri) { + if (!uri.isAbsolute()) { + throw new IllegalArgumentException("Relative URIs are not supported."); + } + } + + public static final class OpenForReadResult { + public final Uri uri; + public final InputStream inputStream; + public final String mimeType; + public final long length; + public final AssetFileDescriptor assetFd; + + OpenForReadResult(Uri uri, InputStream inputStream, String mimeType, long length, AssetFileDescriptor assetFd) { + this.uri = uri; + this.inputStream = inputStream; + this.mimeType = mimeType; + this.length = length; + this.assetFd = assetFd; + } + } +} diff --git a/framework/src/org/apache/cordova/CordovaWebView.java b/framework/src/org/apache/cordova/CordovaWebView.java index 1035f3a7..06cee9cb 100755 --- a/framework/src/org/apache/cordova/CordovaWebView.java +++ b/framework/src/org/apache/cordova/CordovaWebView.java @@ -23,8 +23,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; -import java.util.Stack; -import java.util.regex.Pattern; import org.apache.cordova.Config; import org.apache.cordova.CordovaInterface; @@ -96,6 +94,8 @@ public class CordovaWebView extends WebView { private ActivityResult mResult = null; + private CordovaResourceApi resourceApi; + class ActivityResult { int request; @@ -306,6 +306,7 @@ public class CordovaWebView extends WebView { pluginManager = new PluginManager(this, this.cordova); jsMessageQueue = new NativeToJsMessageQueue(this, cordova); exposedJsApi = new ExposedJsApi(pluginManager, jsMessageQueue); + resourceApi = new CordovaResourceApi(this.getContext(), pluginManager); exposeJsInterface(); } @@ -955,37 +956,7 @@ public class CordovaWebView extends WebView { 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."); - } - UriResolver ret = null; - // 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); - ret = UriResolvers.createError("Whitelist rejection for: " + uri); - } else { - // Give plugins a chance to handle the request. - ret = ((org.apache.cordova.PluginManager)pluginManager).resolveUri(uri); - } - if (ret == null && !fromWebView) { - ret = UriResolvers.forUri(uri, cordova.getActivity()); - if (ret == null) { - ret = UriResolvers.createError("Unresolvable URI: " + uri); - } - } - return ret == null ? null : UriResolvers.makeThreadChecking(ret); + public CordovaResourceApi getResourceApi() { + return resourceApi; } } diff --git a/framework/src/org/apache/cordova/CordovaWebViewClient.java b/framework/src/org/apache/cordova/CordovaWebViewClient.java index 6dbc4ef3..b3a31aea 100755 --- a/framework/src/org/apache/cordova/CordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/CordovaWebViewClient.java @@ -48,7 +48,7 @@ import android.webkit.WebViewClient; */ public class CordovaWebViewClient extends WebViewClient { - private static final String TAG = "Cordova"; + private static final String TAG = "CordovaWebViewClient"; private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/"; CordovaInterface cordova; CordovaWebView appView; diff --git a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java index 90007365..d1e632de 100644 --- a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java @@ -19,20 +19,22 @@ package org.apache.cordova; import java.io.IOException; -import java.io.InputStream; import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaResourceApi.OpenForReadResult; import org.apache.cordova.LOG; import android.annotation.TargetApi; import android.net.Uri; import android.os.Build; +import android.util.Log; import android.webkit.WebResourceResponse; import android.webkit.WebView; @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class IceCreamCordovaWebViewClient extends CordovaWebViewClient { + private static final String TAG = "IceCreamCordovaWebViewClient"; public IceCreamCordovaWebViewClient(CordovaInterface cordova) { super(cordova); @@ -44,38 +46,44 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient { @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { - // Disable checks during shouldInterceptRequest since there is no way to avoid IO here :(. - UriResolvers.webCoreThread = null; + // Tell the Thread-Checking resolve what thread the WebCore thread is. + CordovaResourceApi.webCoreThread = Thread.currentThread(); + Log.e("WHAAAA", "FOOD " + CordovaResourceApi.webCoreThread); try { - UriResolver uriResolver = appView.resolveUri(Uri.parse(url), true); - - if (uriResolver == null && url.startsWith("file:///android_asset/")) { - if (url.contains("?") || url.contains("#") || needsIceCreamSpecialsInAssetUrlFix(url)) { - uriResolver = appView.resolveUri(Uri.parse(url), false); - } + // Check the against the white-list. + if ((url.startsWith("http:") || url.startsWith("https:")) && !Config.isUrlWhiteListed(url)) { + LOG.w(TAG, "URL blocked by whitelist: " + url); + // Results in a 404. + return new WebResourceResponse("text/plain", "UTF-8", null); } + + CordovaResourceApi resourceApi = appView.getResourceApi(); + Uri origUri = Uri.parse(url); + // Allow plugins to intercept WebView requests. + Uri remappedUri = resourceApi.remapUri(origUri); - if (uriResolver != null) { - try { - 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("IceCreamCordovaWebViewClient", "Error occurred while loading a file.", e); - // Results in a 404. - return new WebResourceResponse("text/plain", "UTF-8", null); - } + if (!origUri.equals(remappedUri) || needsSpecialsInAssetUrlFix(origUri)) { + OpenForReadResult result = resourceApi.openForRead(remappedUri); + return new WebResourceResponse(result.mimeType, "UTF-8", result.inputStream); } + // If we don't need to special-case the request, let the browser load it. return null; - } finally { - // Tell the Thread-Checking resolve what thread the WebCore thread is. - UriResolvers.webCoreThread = Thread.currentThread(); + } catch (IOException e) { + LOG.e("IceCreamCordovaWebViewClient", "Error occurred while loading a file.", e); + // Results in a 404. + return new WebResourceResponse("text/plain", "UTF-8", null); } } + + private static boolean needsSpecialsInAssetUrlFix(Uri uri) { + if (CordovaResourceApi.getUriType(uri) != CordovaResourceApi.URI_TYPE_ASSET) { + return false; + } + if (uri.getQuery() != null || uri.getFragment() != null) { + return true; + } - private static boolean needsIceCreamSpecialsInAssetUrlFix(String url) { - if (!url.contains("%20")){ + if (!uri.toString().contains("%")) { return false; } @@ -83,8 +91,7 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient { case android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH: case android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1: return true; - default: - return false; } + return false; } } diff --git a/framework/src/org/apache/cordova/PluginManager.java b/framework/src/org/apache/cordova/PluginManager.java index 768f2530..3d0169ac 100755 --- a/framework/src/org/apache/cordova/PluginManager.java +++ b/framework/src/org/apache/cordova/PluginManager.java @@ -26,7 +26,6 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.cordova.CordovaArgs; import org.apache.cordova.CordovaWebView; -import org.apache.cordova.UriResolver; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; @@ -40,7 +39,6 @@ import android.content.res.XmlResourceParser; import android.net.Uri; import android.util.Log; -import android.webkit.WebResourceResponse; /** * PluginManager is exposed to JavaScript in the Cordova WebView. @@ -394,10 +392,10 @@ public class PluginManager { LOG.e(TAG, "====================================================================================="); } - UriResolver resolveUri(Uri uri) { + Uri remapUri(Uri uri) { for (PluginEntry entry : this.entries.values()) { if (entry.plugin != null) { - UriResolver ret = entry.plugin.resolveUri(uri); + Uri ret = entry.plugin.remapUri(uri); if (ret != null) { return ret; } diff --git a/framework/src/org/apache/cordova/UriResolver.java b/framework/src/org/apache/cordova/UriResolver.java deleted file mode 100644 index b3bfa4d4..00000000 --- a/framework/src/org/apache/cordova/UriResolver.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - 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; - -/* - * Interface for a class that can resolve URIs. - * See CordovaUriResolver for an example. - */ -public abstract class UriResolver { - - /** - * Returns the InputStream for the resource. - * Throws an exception if it cannot be read. - * Never returns null. - */ - public abstract InputStream getInputStream() 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). - */ - public abstract String getMimeType(); - - /** Returns whether the resource is writable. */ - public abstract boolean isWritable(); - - /** - * Returns a File that points to the resource, or null if the resource - * is not on the local file system. - */ - public abstract File getLocalFile(); - - /** - * Returns the OutputStream for the resource. - * Throws an exception if it cannot be written to. - * Never returns null. - */ - public OutputStream getOutputStream() throws IOException { - throw new IOException("Writing is not suppported"); - } - - /** - * Returns the length of the input stream, or -1 if it is not computable. - */ - public long computeLength() throws IOException { - return -1; - } -} diff --git a/framework/src/org/apache/cordova/UriResolvers.java b/framework/src/org/apache/cordova/UriResolvers.java deleted file mode 100644 index 294fc6bf..00000000 --- a/framework/src/org/apache/cordova/UriResolvers.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - 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; -import android.os.Looper; - -/* - * UriResolver implementations. - */ -public final class UriResolvers { - static Thread webCoreThread; - - private UriResolvers() {} - - private static long computeSizeFromResolver(UriResolver resolver) throws IOException { - InputStream inputStream = resolver.getInputStream(); - if (inputStream instanceof FileInputStream) { - return ((FileInputStream)inputStream).getChannel().size(); - } - if (inputStream instanceof ByteArrayInputStream) { - return ((ByteArrayInputStream)inputStream).available(); - } - return -1; - } - - private static final class FileUriResolver extends UriResolver { - private final File localFile; - private String mimeType; - private FileInputStream cachedInputStream; - - FileUriResolver(Uri uri) { - localFile = new File(uri.getPath()); - } - - public InputStream getInputStream() throws IOException { - if (cachedInputStream == null) { - cachedInputStream = new FileInputStream(localFile); - } - return cachedInputStream; - } - - public OutputStream getOutputStream() throws FileNotFoundException { - File parent = localFile.getParentFile(); - if (parent != null) { - localFile.getParentFile().mkdirs(); - } - return new FileOutputStream(localFile); - } - - public String getMimeType() { - if (mimeType == null) { - mimeType = FileHelper.getMimeTypeForExtension(localFile.getName()); - } - return mimeType; - } - - public boolean isWritable() { - if (localFile.isDirectory()) { - return false; - } - if (localFile.exists()) { - return localFile.canWrite(); - } - return localFile.getParentFile().canWrite(); - } - - public File getLocalFile() { - return localFile; - } - - public long computeLength() throws IOException { - return localFile.length(); - } - } - - private static final class AssetUriResolver extends UriResolver { - private final AssetManager assetManager; - private final String assetPath; - private String mimeType; - private InputStream cachedInputStream; - - AssetUriResolver(Uri uri, AssetManager assetManager) { - this.assetManager = assetManager; - this.assetPath = uri.getPath().substring(15); - } - - public InputStream getInputStream() throws IOException { - if (cachedInputStream == null) { - cachedInputStream = assetManager.open(assetPath); - } - return cachedInputStream; - } - - 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; - } - - public long computeLength() throws IOException { - return computeSizeFromResolver(this); - } - } - - private static final class ContentUriResolver extends UriResolver { - private final Uri uri; - private final ContentResolver contentResolver; - private String mimeType; - private InputStream cachedInputStream; - - ContentUriResolver(Uri uri, ContentResolver contentResolver) { - this.uri = uri; - this.contentResolver = contentResolver; - } - - public InputStream getInputStream() throws IOException { - if (cachedInputStream == null) { - cachedInputStream = contentResolver.openInputStream(uri); - } - return cachedInputStream; - } - - 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; - } - - public long computeLength() throws IOException { - return computeSizeFromResolver(this); - } - } - - private static final class ErrorUriResolver extends UriResolver { - final String errorMsg; - - ErrorUriResolver(String errorMsg) { - this.errorMsg = errorMsg; - } - - public boolean isWritable() { - return false; - } - - public File getLocalFile() { - return null; - } - - public OutputStream getOutputStream() throws IOException { - throw new FileNotFoundException(errorMsg); - } - - public String getMimeType() { - return null; - } - - public InputStream getInputStream() throws IOException { - throw new FileNotFoundException(errorMsg); - } - } - - private static final class ReadOnlyResolver extends UriResolver { - private InputStream inputStream; - private String mimeType; - - public ReadOnlyResolver(Uri uri, InputStream inputStream, String mimeType) { - this.inputStream = inputStream; - this.mimeType = mimeType; - } - - public boolean isWritable() { - return false; - } - - public File getLocalFile() { - return null; - } - - public OutputStream getOutputStream() throws IOException { - throw new FileNotFoundException("URI is not writable"); - } - - public String getMimeType() { - return mimeType; - } - - public InputStream getInputStream() throws IOException { - return inputStream; - } - - public long computeLength() throws IOException { - return computeSizeFromResolver(this); - } - } - - private static final class ThreadCheckingResolver extends UriResolver { - final UriResolver delegate; - - ThreadCheckingResolver(UriResolver delegate) { - this.delegate = delegate; - } - - private static void checkThread() { - Thread curThread = Thread.currentThread(); - if (curThread == Looper.getMainLooper().getThread()) { - throw new IllegalStateException("Do not perform IO operations on the UI thread. Use CordovaInterface.getThreadPool() instead."); - } - if (curThread == webCoreThread) { - throw new IllegalStateException("Tried to perform an IO operation on the WebCore thread. Use CordovaInterface.getThreadPool() instead."); - } - } - - public boolean isWritable() { - checkThread(); - return delegate.isWritable(); - } - - - public File getLocalFile() { - checkThread(); - return delegate.getLocalFile(); - } - - public OutputStream getOutputStream() throws IOException { - checkThread(); - return delegate.getOutputStream(); - } - - public String getMimeType() { - checkThread(); - return delegate.getMimeType(); - } - - public InputStream getInputStream() throws IOException { - checkThread(); - return delegate.getInputStream(); - } - - public long computeLength() throws IOException { - checkThread(); - return delegate.computeLength(); - } - } - - 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); - } - - public static UriResolver createError(String errorMsg) { - return new ErrorUriResolver(errorMsg); - } - - /* 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; - } - - /* Used only by CordovaWebView.resolveUri(). */ - static UriResolver makeThreadChecking(UriResolver resolver) { - if (resolver instanceof ThreadCheckingResolver) { - return resolver; - } - return new ThreadCheckingResolver(resolver); - } -} diff --git a/test/AndroidManifest.xml b/test/AndroidManifest.xml index f6c840eb..04ef3c6e 100755 --- a/test/AndroidManifest.xml +++ b/test/AndroidManifest.xml @@ -45,7 +45,7 @@ - + { +public class CordovaResourceApiTest extends ActivityInstrumentationTestCase2 { - public UriResolversTest() + public CordovaResourceApiTest() { super(CordovaWebViewTestActivity.class); } CordovaWebView cordovaWebView; + CordovaResourceApi resourceApi; + private CordovaWebViewTestActivity activity; String execPayload; Integer execStatus; @@ -59,32 +63,26 @@ public class UriResolversTest extends ActivityInstrumentationTestCase2