[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.
This commit is contained in:
Andrew Grieve 2013-07-14 21:39:55 -04:00
parent 210d7c76e6
commit 77e9092108
10 changed files with 466 additions and 539 deletions

View File

@ -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.
*

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -45,7 +45,7 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-sdk android:minSdkVersion="7" />
<uses-sdk android:minSdkVersion="8" />
<instrumentation
android:name="android.test.InstrumentationTestRunner"

View File

@ -22,9 +22,9 @@ package org.apache.cordova.test;
*
*/
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.CordovaResourceApi.OpenForReadResult;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.UriResolver;
import org.apache.cordova.UriResolvers;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginEntry;
@ -34,6 +34,9 @@ import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Scanner;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@ -41,16 +44,17 @@ 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 class CordovaResourceApiTest extends ActivityInstrumentationTestCase2<CordovaWebViewTestActivity> {
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<CordovaWe
super.setUp();
activity = this.getActivity();
cordovaWebView = activity.cordovaWebView;
cordovaWebView.pluginManager.addService(new PluginEntry("UriResolverTestPlugin1", new CordovaPlugin() {
resourceApi = cordovaWebView.getResourceApi();
resourceApi.setThreadCheckingEnabled(false);
cordovaWebView.pluginManager.addService(new PluginEntry("CordovaResourceApiTestPlugin1", new CordovaPlugin() {
@Override
public UriResolver resolveUri(Uri uri) {
if ("plugin-uri".equals(uri.getScheme())) {
return cordovaWebView.resolveUri(uri.buildUpon().scheme("file").build());
public Uri remapUri(Uri uri) {
if (uri.getQuery() != null && uri.getQuery().contains("pluginRewrite")) {
return cordovaWebView.getResourceApi().remapUri(
Uri.parse("data:text/plain;charset=utf-8,pass"));
}
return null;
}
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
synchronized (UriResolversTest.this) {
synchronized (CordovaResourceApiTest.this) {
execPayload = args.getString(0);
execStatus = args.getInt(1);
UriResolversTest.this.notify();
CordovaResourceApiTest.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() {
@ -94,19 +92,15 @@ public class UriResolversTest extends ActivityInstrumentationTestCase2<CordovaWe
return Uri.parse(stored);
}
private void performResolverTest(Uri uri, String expectedMimeType, File expectedLocalFile,
boolean expectedIsWritable,
private void performApiTest(Uri uri, String expectedMimeType, File expectedLocalFile,
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());
}
uri = resourceApi.remapUri(uri);
assertEquals(expectedLocalFile, resourceApi.mapUriToFile(uri));
try {
resolver.getInputStream().read();
OpenForReadResult readResult = resourceApi.openForRead(uri);
assertEquals(expectedMimeType, readResult.mimeType);
readResult.inputStream.read();
if (!expectRead) {
fail("Expected getInputStream to throw.");
}
@ -116,10 +110,12 @@ public class UriResolversTest extends ActivityInstrumentationTestCase2<CordovaWe
}
}
try {
resolver.getOutputStream().write(123);
OutputStream outStream = resourceApi.openOutputStream(uri);
outStream.write(123);
if (!expectWrite) {
fail("Expected getOutputStream to throw.");
}
outStream.close();
} catch (IOException e) {
if (expectWrite) {
throw e;
@ -130,25 +126,27 @@ public class UriResolversTest extends ActivityInstrumentationTestCase2<CordovaWe
public void testValidContentUri() throws IOException
{
Uri contentUri = createTestImageContentUri();
performResolverTest(contentUri, "image/jpeg", null, true, true, true);
File localFile = resourceApi.mapUriToFile(contentUri);
assertNotNull(localFile);
performApiTest(contentUri, "image/jpeg", localFile, true, true);
}
public void testInvalidContentUri() throws IOException
{
Uri contentUri = Uri.parse("content://media/external/images/media/999999999");
performResolverTest(contentUri, null, null, true, false, false);
performApiTest(contentUri, null, null, 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);
performApiTest(assetUri, "text/html", null, 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);
performApiTest(assetUri, "text/html", null, false, false);
}
public void testFileUriToExistingFile() throws IOException
@ -156,7 +154,7 @@ public class UriResolversTest extends ActivityInstrumentationTestCase2<CordovaWe
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);
performApiTest(fileUri, "text/plain", f, true, true);
} finally {
f.delete();
}
@ -167,7 +165,7 @@ public class UriResolversTest extends ActivityInstrumentationTestCase2<CordovaWe
File f = new File(Environment.getExternalStorageDirectory() + "/somefilethatdoesntexist");
Uri fileUri = Uri.parse(f.toURI().toString());
try {
performResolverTest(fileUri, null, f, true, false, true);
performApiTest(fileUri, null, f, false, true);
} finally {
f.delete();
}
@ -175,52 +173,70 @@ public class UriResolversTest extends ActivityInstrumentationTestCase2<CordovaWe
public void testFileUriToMissingFileWithMissingParent() throws IOException
{
File f = new File(Environment.getExternalStorageDirectory() + "/somedirthatismissing/somefilethatdoesntexist");
File f = new File(Environment.getExternalStorageDirectory() + "/somedirthatismissing" + System.currentTimeMillis() + "/somefilethatdoesntexist");
Uri fileUri = Uri.parse(f.toURI().toString());
performResolverTest(fileUri, null, f, false, false, false);
performApiTest(fileUri, null, f, false, true);
}
public void testUnrecognizedUri() throws IOException
{
Uri uri = Uri.parse("somescheme://foo");
performResolverTest(uri, null, null, false, false, false);
performApiTest(uri, null, null, false, false);
}
public void testRelativeUri()
{
try {
cordovaWebView.resolveUri(Uri.parse("/foo"));
resourceApi.openForRead(Uri.parse("/foo"));
fail("Should have thrown for relative URI 1.");
} catch (Throwable t) {
}
try {
cordovaWebView.resolveUri(Uri.parse("//foo/bar"));
resourceApi.openForRead(Uri.parse("//foo/bar"));
fail("Should have thrown for relative URI 2.");
} catch (Throwable t) {
}
try {
cordovaWebView.resolveUri(Uri.parse("foo.png"));
resourceApi.openForRead(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
public void testPluginOverride() throws IOException
{
Uri uri = Uri.parse("plugin-uri://foohost/android_asset/www/index.html?pluginRewrite=yes");
performResolverTest(uri, "my/mime", null, false, true, false);
performApiTest(uri, "text/plain", null, true, false);
}
public void testWhitelistRejection() throws IOException
public void testMainThreadUsage() throws IOException
{
Uri uri = Uri.parse("http://foohost.com/");
performResolverTest(uri, null, null, false, false, false);
Uri assetUri = Uri.parse("file:///android_asset/www/index.html");
resourceApi.setThreadCheckingEnabled(true);
try {
resourceApi.openForRead(assetUri);
fail("Should have thrown for main thread check.");
} catch (Throwable t) {
}
}
public void testDataUriPlain() throws IOException
{
Uri uri = Uri.parse("data:text/plain;charset=utf-8,pass");
OpenForReadResult readResult = resourceApi.openForRead(uri);
assertEquals("text/plain", readResult.mimeType);
String data = new Scanner(readResult.inputStream, "UTF-8").useDelimiter("\\A").next();
assertEquals("pass", data);
}
public void testDataUriBase64() throws IOException
{
Uri uri = Uri.parse("data:text/js;charset=utf-8;base64,cGFzcw==");
OpenForReadResult readResult = resourceApi.openForRead(uri);
assertEquals("text/js", readResult.mimeType);
String data = new Scanner(readResult.inputStream, "UTF-8").useDelimiter("\\A").next();
assertEquals("pass", data);
}
public void testWebViewRequestIntercept() throws IOException
@ -229,7 +245,7 @@ public class UriResolversTest extends ActivityInstrumentationTestCase2<CordovaWe
"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])");
"cordova.require('cordova/exec')(null,null,'CordovaResourceApiTestPlugin1', 'foo', [x.responseText, x.status])");
execPayload = null;
execStatus = null;
try {
@ -248,7 +264,7 @@ public class UriResolversTest extends ActivityInstrumentationTestCase2<CordovaWe
"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])");
"cordova.require('cordova/exec')(null,null,'CordovaResourceApiTestPlugin1', 'foo', [x.responseText, x.status])");
execPayload = null;
execStatus = null;
try {