Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): add WebViewAssetLoader proxy handler for cdvfile #513

Merged
merged 10 commits into from
Mar 15, 2022
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,22 @@ This method will now return filesystem URLs of the form

which can be used to identify the file uniquely.

In v7.0.0 the return value of `toURL()` for Android was updated to return the absolute `file://` URL when app content is served from the `file://` scheme.

If app content is served from the `http(s)://` scheme, a `cdvfile` formatted URL will be returned instead. The `cdvfile` formatted URL is created from the internal method `toInternalURL()`.

An example `toInternalURL()` return filesystem URL:

https://localhost/persistent/path/to/file

[![toURL flow](https://sketchviz.com/@erisu/7b05499842275be93a0581e8e3576798/6dc71d8302cafd05b443d874a592d10fa415b8e3.sketchy.png)](//sketchviz.com/@erisu/7b05499842275be93a0581e8e3576798)

It is recommended to always use the `toURL()` to ensure that the correct URL is returned.

## cdvfile protocol

- Not Supported on Android

**Purpose**

`cdvfile://localhost/persistent|temporary|another-fs-root*/path/to/file` can be used for platform-independent file paths.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"cordova-android": ">=6.3.0"
},
"7.0.0": {
"cordova-android": ">=9.0.0"
"cordova-android": ">=10.0.0"
},
"8.0.0": {
"cordova": ">100"
Expand Down
5 changes: 4 additions & 1 deletion plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ xmlns:android="http://schemas.android.com/apk/res/android"
<issue>https://github.com/apache/cordova-plugin-file/issues</issue>

<engines>
<engine name="cordova-android" version=">=9.0.0" />
<engine name="cordova-android" version=">=10.0.0" />
</engines>

<js-module src="www/DirectoryEntry.js" name="DirectoryEntry">
Expand Down Expand Up @@ -153,6 +153,9 @@ to config.xml in order for the application to find previously stored files.
<source-file src="src/android/AssetFilesystem.java" target-dir="src/org/apache/cordova/file" />
<source-file src="src/android/PendingRequests.java" target-dir="src/org/apache/cordova/file" />

<preference name="ANDROIDX_WEBKIT_VERSION" default="1.4.0"/>
<framework src="androidx.webkit:webkit:$ANDROIDX_WEBKIT_VERSION" />

<!-- android specific file apis -->
<js-module src="www/android/FileSystem.js" name="androidFileSystem">
<merges target="FileSystem" />
Expand Down
12 changes: 6 additions & 6 deletions src/android/AssetFilesystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Licensed to the Apache Software Foundation (ASF) under one
import android.content.res.AssetManager;
import android.net.Uri;

import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.LOG;
import org.json.JSONArray;
Expand Down Expand Up @@ -133,8 +134,8 @@ private long getAssetSize(String assetPath) throws FileNotFoundException {
}
}

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

Expand All @@ -161,10 +162,9 @@ public LocalFilesystemURL toLocalUri(Uri inputURL) {
if (!subPath.isEmpty()) {
subPath = subPath.substring(1);
}
Uri.Builder b = new Uri.Builder()
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
.authority("localhost")
.path(name);

Uri.Builder b = createLocalUriBuilder();

if (!subPath.isEmpty()) {
b.appendEncodedPath(subPath);
}
Expand Down
14 changes: 7 additions & 7 deletions src/android/ContentFilesystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Licensed to the Apache Software Foundation (ASF) under one
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaResourceApi;
import org.json.JSONException;
import org.json.JSONObject;
Expand All @@ -36,8 +38,8 @@ public class ContentFilesystem extends Filesystem {

private final Context context;

public ContentFilesystem(Context context, CordovaResourceApi resourceApi) {
super(Uri.parse("content://"), "content", resourceApi);
public ContentFilesystem(Context context, CordovaResourceApi resourceApi, CordovaPreferences preferences) {
super(Uri.parse("content://"), "content", resourceApi, preferences);
this.context = context;
}

Expand Down Expand Up @@ -68,11 +70,9 @@ public LocalFilesystemURL toLocalUri(Uri inputURL) {
if (subPath.length() > 0) {
subPath = subPath.substring(1);
}
Uri.Builder b = new Uri.Builder()
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
.authority("localhost")
.path(name)
.appendPath(inputURL.getAuthority());

Uri.Builder b = createLocalUriBuilder().appendPath(inputURL.getAuthority());

if (subPath.length() > 0) {
b.appendEncodedPath(subPath);
}
Expand Down
117 changes: 100 additions & 17 deletions src/android/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,23 @@ Licensed to the Apache Software Foundation (ASF) under one

import android.Manifest;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.util.Base64;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.webkit.WebResourceResponse;

import androidx.webkit.WebViewAssetLoader;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaPluginPathHandler;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.LOG;
import org.apache.cordova.PermissionHelper;
Expand All @@ -39,12 +46,16 @@ Licensed to the Apache Software Foundation (ASF) under one
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Permission;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -87,8 +98,6 @@ public class FileUtils extends CordovaPlugin {

private PendingRequests pendingRequests;



/*
* We need both read and write when accessing the storage, I think.
*/
Expand Down Expand Up @@ -136,10 +145,10 @@ protected void registerExtraFileSystems(String[] filesystems, HashMap<String, St
if (fsRoot != null) {
File newRoot = new File(fsRoot);
if (newRoot.mkdirs() || newRoot.isDirectory()) {
registerFilesystem(new LocalFilesystem(fsName, webView.getContext(), webView.getResourceApi(), newRoot));
registerFilesystem(new LocalFilesystem(fsName, webView.getContext(), webView.getResourceApi(), newRoot, preferences));
installedFileSystems.add(fsName);
} else {
LOG.d(LOG_TAG, "Unable to create root dir for filesystem \"" + fsName + "\", skipping");
LOG.d(LOG_TAG, "Unable to create root dir for filesystem \"" + fsName + "\", skipping");
}
} else {
LOG.d(LOG_TAG, "Unrecognized extra filesystem identifier: " + fsName);
Expand Down Expand Up @@ -217,10 +226,10 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
// Note: The temporary and persistent filesystems need to be the first two
// registered, so that they will match window.TEMPORARY and window.PERSISTENT,
// per spec.
this.registerFilesystem(new LocalFilesystem("temporary", webView.getContext(), webView.getResourceApi(), tmpRootFile));
this.registerFilesystem(new LocalFilesystem("persistent", webView.getContext(), webView.getResourceApi(), persistentRootFile));
this.registerFilesystem(new ContentFilesystem(webView.getContext(), webView.getResourceApi()));
this.registerFilesystem(new AssetFilesystem(webView.getContext().getAssets(), webView.getResourceApi()));
this.registerFilesystem(new LocalFilesystem("temporary", webView.getContext(), webView.getResourceApi(), tmpRootFile, preferences));
this.registerFilesystem(new LocalFilesystem("persistent", webView.getContext(), webView.getResourceApi(), persistentRootFile, preferences));
this.registerFilesystem(new ContentFilesystem(webView.getContext(), webView.getResourceApi(), preferences));
this.registerFilesystem(new AssetFilesystem(webView.getContext().getAssets(), webView.getResourceApi(), preferences));

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

Expand Down Expand Up @@ -249,13 +258,15 @@ public Uri remapUri(Uri uri) {
if (!LocalFilesystemURL.FILESYSTEM_PROTOCOL.equals(uri.getScheme())) {
return null;
}

try {
LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri);
Filesystem fs = this.filesystemForURL(inputURL);
if (fs == null) {
return null;
}
String path = fs.filesystemPathForURL(inputURL);

if (path != null) {
return Uri.parse("file://" + fs.filesystemPathForURL(inputURL));
}
Expand All @@ -270,6 +281,7 @@ public boolean execute(String action, final String rawArgs, final CallbackContex
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, "File plugin is not configured. Please see the README.md file for details on how to update config.xml"));
return true;
}

if (action.equals("testSaveLocationExists")) {
threadhelper(new FileOp() {
public void run(JSONArray args) {
Expand Down Expand Up @@ -459,18 +471,24 @@ else if (action.equals("getFile")) {
public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
String dirname = args.getString(0);
String path = args.getString(1);
String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);

if(containsCreate && needPermission(nativeURL, WRITE)) {
getWritePermission(rawArgs, ACTION_GET_FILE, callbackContext);
}
else if(!containsCreate && needPermission(nativeURL, READ)) {
getReadPermission(rawArgs, ACTION_GET_FILE, callbackContext);
}
else {
if (dirname.contains(LocalFilesystemURL.CDVFILE_KEYWORD) == true) {
JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
callbackContext.success(obj);
} else {
String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);

if(containsCreate && needPermission(nativeURL, WRITE)) {
getWritePermission(rawArgs, ACTION_GET_FILE, callbackContext);
}
else if(!containsCreate && needPermission(nativeURL, READ)) {
getReadPermission(rawArgs, ACTION_GET_FILE, callbackContext);
}
else {
JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
callbackContext.success(obj);
}
}
}
}, rawArgs, callbackContext);
Expand Down Expand Up @@ -878,6 +896,7 @@ private boolean remove(String baseURLstr) throws NoModificationAllowedException,
private JSONObject getFile(String baseURLstr, String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
try {
LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);

Filesystem fs = this.filesystemForURL(inputURL);
if (fs == null) {
throw new MalformedURLException("No installed handlers for this URL");
Expand Down Expand Up @@ -1222,4 +1241,68 @@ public void run(JSONArray args) throws JSONException, FileNotFoundException, IOE
LOG.d(LOG_TAG, "Received permission callback for unknown request code");
}
}

private String getMimeType(Uri uri) {
String fileExtensionFromUrl = MimeTypeMap.getFileExtensionFromUrl(uri.toString()).toLowerCase();
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtensionFromUrl);
}

public CordovaPluginPathHandler getPathHandler() {
WebViewAssetLoader.PathHandler pathHandler = path -> {
String targetFileSystem = null;

if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("persistent"))) {
targetFileSystem = "persistent";
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("temporary"))) {
targetFileSystem = "temporary";
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("files"))) {
targetFileSystem = "files";
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("documents"))) {
targetFileSystem = "documents";
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("cache"))) {
targetFileSystem = "cache";
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("root"))) {
targetFileSystem = "root";
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("files-external"))) {
targetFileSystem = "files-external";
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("sdcard"))) {
targetFileSystem = "sdcard";
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("cache-external"))) {
targetFileSystem = "cache-external";
}

if (targetFileSystem != null) {
// Loop the registered file systems to find the target.
for (Filesystem fileSystem : filesystems) {

/*
* When target is discovered:
* 1. Transform the url path to the native path
* 2. Load the file contents
* 3. Get the file mime type
* 4. Return the file & mime information back we Web Resources
*/
if (fileSystem.name.equals(targetFileSystem)) {
// E.g. replace __cdvfile_persistent__ with native path "/data/user/0/com.example.file/files/files/"
String fileSystemNativeUri = fileSystem.rootUri.toString().replace("file://", "");
String fileTarget = path.replace(LocalFilesystemURL.fsNameToCdvKeyword(targetFileSystem) + "/", fileSystemNativeUri);

File file = new File(fileTarget);

try {
InputStream in = new FileInputStream(file);
String mimeType = getMimeType(Uri.parse(file.toString()));
return new WebResourceResponse(mimeType, null, in);
} catch (FileNotFoundException e) {
Log.e(LOG_TAG, e.getMessage());
}
}
}
}

return null;
};

return new CordovaPluginPathHandler(pathHandler);
}
}
19 changes: 18 additions & 1 deletion src/android/Filesystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Licensed to the Apache Software Foundation (ASF) under one
import java.util.ArrayList;
import java.util.Arrays;

import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaResourceApi;
import org.json.JSONArray;
import org.json.JSONException;
Expand All @@ -38,13 +39,18 @@ public abstract class Filesystem {

protected final Uri rootUri;
protected final CordovaResourceApi resourceApi;
protected final CordovaPreferences preferences;
public final String name;
private JSONObject rootEntry;

public Filesystem(Uri rootUri, String name, CordovaResourceApi resourceApi) {
static String SCHEME_HTTPS = "https";
static String DEFAULT_HOSTNAME = "localhost";

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

public interface ReadFileCallback {
Expand Down Expand Up @@ -328,4 +334,15 @@ public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException
return numBytesRead;
}
}

protected Uri.Builder createLocalUriBuilder() {
String scheme = preferences.getString("scheme", SCHEME_HTTPS).toLowerCase();
String hostname = preferences.getString("hostname", DEFAULT_HOSTNAME).toLowerCase();
String path = LocalFilesystemURL.fsNameToCdvKeyword(name);

return new Uri.Builder()
.scheme(scheme)
.authority(hostname)
.path(path);
}
}
Loading