Skip to content

Commit

Permalink
feat(android): add WebViewAssetLoader proxy handler for cdvfile (apac…
Browse files Browse the repository at this point in the history
…he#513)

* feat: add WebAssetLoader proxy handler for cdvfile
* fix: update the fileTarget replace string
* chore: make androidx.webkit:webkit configurable & default to 1.4.0
* feat: toURL to return file or custom scheme based on window location
* chore: remove unused variable
* chore: add other file systems to check
* chore: remove comment
* feat: bump cordova-android requirement to >=10.0.0 for AndroidX usage
* doc: updated readme to include the Android changes
  • Loading branch information
erisu authored Mar 15, 2022
1 parent 3e58876 commit 3366ea6
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 50 deletions.
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

0 comments on commit 3366ea6

Please sign in to comment.