Skip to content

Commit

Permalink
CB-6428 android: Add support for file:///android_asset URLs
Browse files Browse the repository at this point in the history
Supports:
- listing
- copying of file
  • Loading branch information
agrieve committed Mar 11, 2015
1 parent d620226 commit 356d334
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 75 deletions.
1 change: 1 addition & 0 deletions plugin.xml
Expand Up @@ -129,6 +129,7 @@ xmlns:android="http://schemas.android.com/apk/res/android"
<source-file src="src/android/Filesystem.java" target-dir="src/org/apache/cordova/file" />
<source-file src="src/android/LocalFilesystem.java" target-dir="src/org/apache/cordova/file" />
<source-file src="src/android/ContentFilesystem.java" target-dir="src/org/apache/cordova/file" />
<source-file src="src/android/AssetFilesystem.java" target-dir="src/org/apache/cordova/file" />

<!-- android specific file apis -->
<js-module src="www/android/FileSystem.js" name="androidFileSystem">
Expand Down
222 changes: 222 additions & 0 deletions src/android/AssetFilesystem.java
@@ -0,0 +1,222 @@
/*
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.file;

import android.content.res.AssetManager;
import android.net.Uri;

import org.apache.cordova.CordovaResourceApi;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

public class AssetFilesystem extends Filesystem {

private final AssetManager assetManager;

public AssetFilesystem(AssetManager assetManager, CordovaResourceApi resourceApi) {
super(Uri.parse("file:///android_asset/"), "assets", resourceApi);
this.assetManager = assetManager;
}

@Override
public Uri toNativeUri(LocalFilesystemURL inputURL) {
return nativeUriForFullPath(inputURL.path);
}

@Override
public LocalFilesystemURL toLocalUri(Uri inputURL) {
if (!"file".equals(inputURL.getScheme())) {
return null;
}
File f = new File(inputURL.getPath());
// Removes and duplicate /s (e.g. file:///a//b/c)
Uri resolvedUri = Uri.fromFile(f);
String rootUriNoTrailingSlash = rootUri.getEncodedPath();
rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1);
if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) {
return null;
}
String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length());
// Strip leading slash
if (!subPath.isEmpty()) {
subPath = subPath.substring(1);
}
Uri.Builder b = new Uri.Builder()
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
.authority("localhost")
.path(name);
if (!subPath.isEmpty()) {
b.appendEncodedPath(subPath);
}
if (isDirectory(subPath) || inputURL.getPath().endsWith("/")) {
// Add trailing / for directories.
b.appendEncodedPath("");
}
return LocalFilesystemURL.parse(b.build());
}

private Boolean isDirectory(String assetPath) {
if (assetPath.startsWith("/")) {
assetPath = assetPath.substring(1);
}
try {
return assetManager.list(assetPath).length != 0;
} catch (IOException e) {
}
return false;
}

private LocalFilesystemURL URLforFullPath(String fullPath) {
Uri nativeUri = nativeUriForFullPath(fullPath);
if (nativeUri != null) {
return toLocalUri(nativeUri);
}
return null;
}


@Override
public JSONArray readEntriesAtLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
String[] files;
String pathNoSlashes = inputURL.path.substring(1);
if (pathNoSlashes.endsWith("/")) {
pathNoSlashes = pathNoSlashes.substring(0, pathNoSlashes.length() - 1);
}

try {
files = assetManager.list(pathNoSlashes);
} catch (IOException e) {
throw new FileNotFoundException();
}

JSONArray entries = new JSONArray();
if (files != null) {
for (String file : files) {
Uri newNativeUri = nativeUriForFullPath(new File(inputURL.path, file).getPath());
entries.put(makeEntryForNativeUri(newNativeUri));
}
}

return entries;
}

@Override
public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
String path, JSONObject options, boolean directory)
throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
if (options != null && options.optBoolean("create")) {
throw new UnsupportedOperationException("Assets are read-only");
}

// Check whether the supplied path is absolute or relative
if (directory && !path.endsWith("/")) {
path += "/";
}

LocalFilesystemURL requestedURL;
if (path.startsWith("/")) {
requestedURL = URLforFullPath(normalizePath(path));
} else {
requestedURL = URLforFullPath(normalizePath(inputURL.path + "/" + path));
}

// Throws a FileNotFoundException if it doesn't exist.
getFileMetadataForLocalURL(requestedURL);

boolean isDir = isDirectory(requestedURL.path);
if (directory && !isDir) {
throw new TypeMismatchException("path doesn't exist or is file");
} else if (!directory && isDir) {
throw new TypeMismatchException("path doesn't exist or is directory");
}

// Return the directory
return makeEntryForURL(requestedURL);
}


@Override
public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
CordovaResourceApi.OpenForReadResult offr;
try {
offr = inputURL.isDirectory ? null : resourceApi.openForRead(toNativeUri(inputURL));
} catch (IOException e) {
throw new FileNotFoundException("File not found: " + inputURL);
}
JSONObject metadata = new JSONObject();
try {
metadata.put("size", inputURL.isDirectory ? 0 : offr.length);
metadata.put("type", inputURL.isDirectory ? "text/directory" : offr.mimeType);
metadata.put("name", new File(inputURL.path).getName());
metadata.put("fullPath", inputURL.path);
metadata.put("lastModifiedDate", 0);
} catch (JSONException e) {
return null;
} finally {
if (offr != null) {
try {
offr.inputStream.close();
} catch (IOException e) {
}
}
}
return metadata;
}

@Override
public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
return false;
}

@Override
long writeToFileAtURL(LocalFilesystemURL inputURL, String data, int offset, boolean isBinary) throws NoModificationAllowedException, IOException {
throw new NoModificationAllowedException("Assets are read-only");
}

@Override
long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException, NoModificationAllowedException {
throw new NoModificationAllowedException("Assets are read-only");
}

@Override
String filesystemPathForURL(LocalFilesystemURL url) {
return null;
}

@Override
LocalFilesystemURL URLforFilesystemPath(String path) {
return null;
}

@Override
boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException, NoModificationAllowedException {
throw new NoModificationAllowedException("Assets are read-only");
}

@Override
boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException, NoModificationAllowedException {
throw new NoModificationAllowedException("Assets are read-only");
}

}
6 changes: 5 additions & 1 deletion src/android/FileUtils.java
Expand Up @@ -194,6 +194,7 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
this.registerFilesystem(new LocalFilesystem("temporary", webView.getContext(), webView.getResourceApi(), tempRoot));
this.registerFilesystem(new LocalFilesystem("persistent", webView.getContext(), webView.getResourceApi(), persistentRoot));
this.registerFilesystem(new ContentFilesystem(webView.getContext(), webView.getResourceApi()));
this.registerFilesystem(new AssetFilesystem(webView.getContext().getAssets(), webView.getResourceApi()));

registerExtraFileSystems(getExtraFileSystemsPreference(activity), getAvailableFileSystems(activity));

Expand Down Expand Up @@ -623,10 +624,13 @@ private JSONObject resolveLocalFileSystemURI(String uriString) throws IOExceptio
if (fs == null) {
throw new MalformedURLException("No installed handlers for this URL");
}
return fs.getEntryForLocalURL(inputURL);
if (fs.exists(inputURL)) {
return fs.getEntryForLocalURL(inputURL);
}
} catch (IllegalArgumentException e) {
throw new MalformedURLException("Unrecognized filesystem URL");
}
throw new FileNotFoundException();
}

/**
Expand Down
74 changes: 64 additions & 10 deletions src/android/Filesystem.java
Expand Up @@ -26,6 +26,8 @@ Licensed to the Apache Software Foundation (ASF) under one
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;

import org.apache.cordova.CordovaResourceApi;
import org.json.JSONArray;
Expand All @@ -37,13 +39,12 @@ public abstract class Filesystem {
protected final Uri rootUri;
protected final CordovaResourceApi resourceApi;
public final String name;
private final JSONObject rootEntry;
private JSONObject rootEntry;

public Filesystem(Uri rootUri, String name, CordovaResourceApi resourceApi) {
this.rootUri = rootUri;
this.name = name;
this.resourceApi = resourceApi;
rootEntry = makeEntryForNativeUri(rootUri);
}

public interface ReadFileCallback {
Expand Down Expand Up @@ -94,6 +95,10 @@ public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOExce
return makeEntryForURL(inputURL);
}

public JSONObject makeEntryForFile(File file) {
return makeEntryForNativeUri(Uri.fromFile(file));
}

abstract JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, String path,
JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException;

Expand All @@ -109,10 +114,67 @@ public Uri getRootUri() {
return rootUri;
}

public boolean exists(LocalFilesystemURL inputURL) {
try {
getFileMetadataForLocalURL(inputURL);
} catch (FileNotFoundException e) {
return false;
}
return true;
}

public Uri nativeUriForFullPath(String fullPath) {
Uri ret = null;
if (fullPath != null) {
String encodedPath = Uri.fromFile(new File(fullPath)).getEncodedPath();
if (encodedPath.startsWith("/")) {
encodedPath = encodedPath.substring(1);
}
ret = rootUri.buildUpon().appendEncodedPath(encodedPath).build();
}
return ret;
}

/**
* Removes multiple repeated //s, and collapses processes ../s.
*/
protected static String normalizePath(String rawPath) {
// If this is an absolute path, trim the leading "/" and replace it later
boolean isAbsolutePath = rawPath.startsWith("/");
if (isAbsolutePath) {
rawPath = rawPath.replaceFirst("/+", "");
}
ArrayList<String> components = new ArrayList<String>(Arrays.asList(rawPath.split("/+")));
for (int index = 0; index < components.size(); ++index) {
if (components.get(index).equals("..")) {
components.remove(index);
if (index > 0) {
components.remove(index-1);
--index;
}
}
}
StringBuilder normalizedPath = new StringBuilder();
for(String component: components) {
normalizedPath.append("/");
normalizedPath.append(component);
}
if (isAbsolutePath) {
return normalizedPath.toString();
} else {
return normalizedPath.toString().substring(1);
}
}



public abstract Uri toNativeUri(LocalFilesystemURL inputURL);
public abstract LocalFilesystemURL toLocalUri(Uri inputURL);

public JSONObject getRootEntry() {
if (rootEntry == null) {
rootEntry = makeEntryForNativeUri(rootUri);
}
return rootEntry;
}

Expand Down Expand Up @@ -235,12 +297,4 @@ public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException
return numBytesRead;
}
}

/* Create a FileEntry or DirectoryEntry given an actual file on device.
* Return null if the file does not exist within this filesystem.
*/
public JSONObject makeEntryForFile(File file) throws JSONException {
return null;
}

}

0 comments on commit 356d334

Please sign in to comment.