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
2 changes: 2 additions & 0 deletions plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ 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" />

<framework src="androidx.webkit:webkit:1.3.0" type="gradleReference" />
breautek marked this conversation as resolved.
Show resolved Hide resolved
erisu marked this conversation as resolved.
Show resolved Hide resolved

<!-- android specific file apis -->
<js-module src="www/android/FileSystem.js" name="androidFileSystem">
<merges target="FileSystem" />
Expand Down
5 changes: 3 additions & 2 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 Down
6 changes: 4 additions & 2 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
77 changes: 70 additions & 7 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,7 +145,7 @@ 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");
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 Down Expand Up @@ -1222,4 +1233,56 @@ 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;

// currently only supports persistent & temporary
if (path.startsWith("__cdvfile_persistent__")) {
targetFileSystem = "persistent";
} else if (path.startsWith("__cdvfile_temporary__")) {
targetFileSystem = "temporary";
}

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("__cdvfile_" + 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;
};

Log.d(LOG_TAG, "Added CDVFile Proxy");
return new CordovaPluginPathHandler(pathHandler);
}
}
32 changes: 28 additions & 4 deletions src/android/Filesystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Licensed to the Apache Software Foundation (ASF) under one
package org.apache.cordova.file;

import android.net.Uri;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
Expand All @@ -29,6 +30,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,20 +40,25 @@ 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) {
private static String SCHEME_HTTPS = "https";
private 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 {
public void handleData(InputStream inputStream, String contentType) throws IOException;
}

public static JSONObject makeEntryForURL(LocalFilesystemURL inputURL, Uri nativeURL) {
public static JSONObject makeEntryForURL(LocalFilesystemURL inputURL, Uri nativeURL, CordovaPreferences preferences) {
try {
String path = inputURL.path;
int end = path.endsWith("/") ? 1 : 0;
Expand All @@ -74,6 +81,23 @@ public static JSONObject makeEntryForURL(LocalFilesystemURL inputURL, Uri native
nativeUrlStr += "/";
}
entry.put("nativeURL", nativeUrlStr);

String cdvURL = "";

if (!preferences.getBoolean("AndroidInsecureFileModeEnabled", false)) {
String scheme = preferences.getString("scheme", SCHEME_HTTPS).toLowerCase();
String hostname = preferences.getString("hostname", DEFAULT_HOSTNAME);

if (!inputURL.isDirectory) {
if (inputURL.fsName.equals("persistent")) {
cdvURL = scheme + "://" + hostname + "/__cdvfile_persistent__" + path;
} else if (inputURL.fsName.equals("temporary")) {
cdvURL = scheme + "://" + hostname + "/__cdvfile_temporary__" + path;
}
}
}

entry.put("cdvURL", cdvURL);
return entry;
} catch (JSONException e) {
e.printStackTrace();
Expand All @@ -83,12 +107,12 @@ public static JSONObject makeEntryForURL(LocalFilesystemURL inputURL, Uri native

public JSONObject makeEntryForURL(LocalFilesystemURL inputURL) {
Uri nativeUri = toNativeUri(inputURL);
return nativeUri == null ? null : makeEntryForURL(inputURL, nativeUri);
return nativeUri == null ? null : makeEntryForURL(inputURL, nativeUri, preferences);
}

public JSONObject makeEntryForNativeUri(Uri nativeUri) {
LocalFilesystemURL inputUrl = toLocalUri(nativeUri);
return inputUrl == null ? null : makeEntryForURL(inputUrl, nativeUri);
return inputUrl == null ? null : makeEntryForURL(inputUrl, nativeUri, preferences);
}

public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOException {
Expand Down
6 changes: 4 additions & 2 deletions src/android/LocalFilesystem.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.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaResourceApi;
import org.json.JSONException;
import org.json.JSONObject;
Expand All @@ -44,8 +46,8 @@ Licensed to the Apache Software Foundation (ASF) under one
public class LocalFilesystem extends Filesystem {
private final Context context;

public LocalFilesystem(String name, Context context, CordovaResourceApi resourceApi, File fsRoot) {
super(Uri.fromFile(fsRoot).buildUpon().appendEncodedPath("").build(), name, resourceApi);
public LocalFilesystem(String name, Context context, CordovaResourceApi resourceApi, File fsRoot, CordovaPreferences preferences) {
super(Uri.fromFile(fsRoot).buildUpon().appendEncodedPath("").build(), name, resourceApi, preferences);
this.context = context;
}

Expand Down
2 changes: 1 addition & 1 deletion www/DirectoryEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ DirectoryEntry.prototype.getFile = function (path, options, successCallback, err
var fs = this.filesystem;
var win = successCallback && function (result) {
var FileEntry = require('./FileEntry');
var entry = new FileEntry(result.name, result.fullPath, fs, result.nativeURL);
var entry = new FileEntry(result.name, result.fullPath, fs, result.nativeURL, result.cdvURL);
successCallback(entry);
};
var fail = errorCallback && function (code) {
Expand Down
11 changes: 10 additions & 1 deletion www/Entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@ var Metadata = require('./Metadata');
* webview controls, for example media players.
* (optional, readonly)
*/
function Entry (isFile, isDirectory, name, fullPath, fileSystem, nativeURL) {
function Entry (isFile, isDirectory, name, fullPath, fileSystem, nativeURL, cdvURL) {
this.isFile = !!isFile;
this.isDirectory = !!isDirectory;
this.name = name || '';
this.fullPath = fullPath || '';
this.filesystem = fileSystem || null;
this.nativeURL = nativeURL || null;
this.cdvURL = cdvURL || null;
}

/**
Expand Down Expand Up @@ -198,6 +199,14 @@ Entry.prototype.toURL = function () {
return this.toInternalURL() || 'file://localhost' + this.fullPath;
};

Entry.prototype.getCdvURL = function () {
if (this.cdvURL) {
return this.cdvURL;
}

return null;
};

/**
* Backwards-compatibility: In v1.0.0 - 1.0.2, .toURL would only return a
* cdvfile:// URL, and this method was necessary to obtain URLs usable by the
Expand Down
7 changes: 5 additions & 2 deletions www/FileEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,19 @@ var FileError = require('./FileError');
* {DOMString} fullPath the absolute full path to the file (readonly)
* {FileSystem} filesystem on which the file resides (readonly)
*/
var FileEntry = function (name, fullPath, fileSystem, nativeURL) {
var FileEntry = function (name, fullPath, fileSystem, nativeURL, cdvURL) {
// remove trailing slash if it is present
if (fullPath && /\/$/.test(fullPath)) {
fullPath = fullPath.substring(0, fullPath.length - 1);
}
if (nativeURL && /\/$/.test(nativeURL)) {
nativeURL = nativeURL.substring(0, nativeURL.length - 1);
}
if (cdvURL && /\/$/.test(cdvURL)) {
cdvURL = cdvURL.substring(0, cdvURL.length - 1);
}

FileEntry.__super__.constructor.apply(this, [true, false, name, fullPath, fileSystem, nativeURL]);
FileEntry.__super__.constructor.apply(this, [true, false, name, fullPath, fileSystem, nativeURL, cdvURL]);
};

utils.extend(FileEntry, Entry);
Expand Down
2 changes: 1 addition & 1 deletion www/resolveLocalFileSystemURI.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
if (!fs) {
fs = new FileSystem(fsName, { name: '', fullPath: '/' }); // eslint-disable-line no-undef
}
var result = (entry.isDirectory) ? new DirectoryEntry(entry.name, entry.fullPath, fs, entry.nativeURL) : new FileEntry(entry.name, entry.fullPath, fs, entry.nativeURL);
var result = (entry.isDirectory) ? new DirectoryEntry(entry.name, entry.fullPath, fs, entry.nativeURL) : new FileEntry(entry.name, entry.fullPath, fs, entry.nativeURL, entry.cdvURL);
successCallback(result);
});
}
Expand Down