Skip to content

Commit 3366ea6

Browse files
authored
feat(android): add WebViewAssetLoader proxy handler for cdvfile (#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
1 parent 3e58876 commit 3366ea6

11 files changed

+174
-50
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,22 @@ This method will now return filesystem URLs of the form
420420

421421
which can be used to identify the file uniquely.
422422

423+
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.
424+
425+
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()`.
426+
427+
An example `toInternalURL()` return filesystem URL:
428+
429+
https://localhost/persistent/path/to/file
430+
431+
[![toURL flow](https://sketchviz.com/@erisu/7b05499842275be93a0581e8e3576798/6dc71d8302cafd05b443d874a592d10fa415b8e3.sketchy.png)](//sketchviz.com/@erisu/7b05499842275be93a0581e8e3576798)
432+
433+
It is recommended to always use the `toURL()` to ensure that the correct URL is returned.
434+
423435
## cdvfile protocol
436+
437+
- Not Supported on Android
438+
424439
**Purpose**
425440

426441
`cdvfile://localhost/persistent|temporary|another-fs-root*/path/to/file` can be used for platform-independent file paths.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"cordova-android": ">=6.3.0"
3838
},
3939
"7.0.0": {
40-
"cordova-android": ">=9.0.0"
40+
"cordova-android": ">=10.0.0"
4141
},
4242
"8.0.0": {
4343
"cordova": ">100"

plugin.xml

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ xmlns:android="http://schemas.android.com/apk/res/android"
3030
<issue>https://github.com/apache/cordova-plugin-file/issues</issue>
3131

3232
<engines>
33-
<engine name="cordova-android" version=">=9.0.0" />
33+
<engine name="cordova-android" version=">=10.0.0" />
3434
</engines>
3535

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

156+
<preference name="ANDROIDX_WEBKIT_VERSION" default="1.4.0"/>
157+
<framework src="androidx.webkit:webkit:$ANDROIDX_WEBKIT_VERSION" />
158+
156159
<!-- android specific file apis -->
157160
<js-module src="www/android/FileSystem.js" name="androidFileSystem">
158161
<merges target="FileSystem" />

src/android/AssetFilesystem.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Licensed to the Apache Software Foundation (ASF) under one
2121
import android.content.res.AssetManager;
2222
import android.net.Uri;
2323

24+
import org.apache.cordova.CordovaPreferences;
2425
import org.apache.cordova.CordovaResourceApi;
2526
import org.apache.cordova.LOG;
2627
import org.json.JSONArray;
@@ -133,8 +134,8 @@ private long getAssetSize(String assetPath) throws FileNotFoundException {
133134
}
134135
}
135136

136-
public AssetFilesystem(AssetManager assetManager, CordovaResourceApi resourceApi) {
137-
super(Uri.parse("file:///android_asset/"), "assets", resourceApi);
137+
public AssetFilesystem(AssetManager assetManager, CordovaResourceApi resourceApi, CordovaPreferences preferences) {
138+
super(Uri.parse("file:///android_asset/"), "assets", resourceApi, preferences);
138139
this.assetManager = assetManager;
139140
}
140141

@@ -161,10 +162,9 @@ public LocalFilesystemURL toLocalUri(Uri inputURL) {
161162
if (!subPath.isEmpty()) {
162163
subPath = subPath.substring(1);
163164
}
164-
Uri.Builder b = new Uri.Builder()
165-
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
166-
.authority("localhost")
167-
.path(name);
165+
166+
Uri.Builder b = createLocalUriBuilder();
167+
168168
if (!subPath.isEmpty()) {
169169
b.appendEncodedPath(subPath);
170170
}

src/android/ContentFilesystem.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Licensed to the Apache Software Foundation (ASF) under one
2828
import java.io.File;
2929
import java.io.FileNotFoundException;
3030
import java.io.IOException;
31+
32+
import org.apache.cordova.CordovaPreferences;
3133
import org.apache.cordova.CordovaResourceApi;
3234
import org.json.JSONException;
3335
import org.json.JSONObject;
@@ -36,8 +38,8 @@ public class ContentFilesystem extends Filesystem {
3638

3739
private final Context context;
3840

39-
public ContentFilesystem(Context context, CordovaResourceApi resourceApi) {
40-
super(Uri.parse("content://"), "content", resourceApi);
41+
public ContentFilesystem(Context context, CordovaResourceApi resourceApi, CordovaPreferences preferences) {
42+
super(Uri.parse("content://"), "content", resourceApi, preferences);
4143
this.context = context;
4244
}
4345

@@ -68,11 +70,9 @@ public LocalFilesystemURL toLocalUri(Uri inputURL) {
6870
if (subPath.length() > 0) {
6971
subPath = subPath.substring(1);
7072
}
71-
Uri.Builder b = new Uri.Builder()
72-
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
73-
.authority("localhost")
74-
.path(name)
75-
.appendPath(inputURL.getAuthority());
73+
74+
Uri.Builder b = createLocalUriBuilder().appendPath(inputURL.getAuthority());
75+
7676
if (subPath.length() > 0) {
7777
b.appendEncodedPath(subPath);
7878
}

src/android/FileUtils.java

+100-17
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,23 @@ Licensed to the Apache Software Foundation (ASF) under one
2020

2121
import android.Manifest;
2222
import android.app.Activity;
23+
import android.content.ContentResolver;
2324
import android.content.Context;
2425
import android.content.pm.PackageManager;
2526
import android.net.Uri;
2627
import android.os.Build;
2728
import android.os.Environment;
2829
import android.util.Base64;
30+
import android.util.Log;
31+
import android.webkit.MimeTypeMap;
32+
import android.webkit.WebResourceResponse;
33+
34+
import androidx.webkit.WebViewAssetLoader;
2935

3036
import org.apache.cordova.CallbackContext;
3137
import org.apache.cordova.CordovaInterface;
3238
import org.apache.cordova.CordovaPlugin;
39+
import org.apache.cordova.CordovaPluginPathHandler;
3340
import org.apache.cordova.CordovaWebView;
3441
import org.apache.cordova.LOG;
3542
import org.apache.cordova.PermissionHelper;
@@ -39,12 +46,16 @@ Licensed to the Apache Software Foundation (ASF) under one
3946
import org.json.JSONException;
4047
import org.json.JSONObject;
4148

49+
import java.io.BufferedInputStream;
4250
import java.io.ByteArrayOutputStream;
4351
import java.io.File;
52+
import java.io.FileInputStream;
4453
import java.io.FileNotFoundException;
4554
import java.io.IOException;
4655
import java.io.InputStream;
56+
import java.net.HttpURLConnection;
4757
import java.net.MalformedURLException;
58+
import java.net.URL;
4859
import java.security.Permission;
4960
import java.util.ArrayList;
5061
import java.util.HashMap;
@@ -87,8 +98,6 @@ public class FileUtils extends CordovaPlugin {
8798

8899
private PendingRequests pendingRequests;
89100

90-
91-
92101
/*
93102
* We need both read and write when accessing the storage, I think.
94103
*/
@@ -136,10 +145,10 @@ protected void registerExtraFileSystems(String[] filesystems, HashMap<String, St
136145
if (fsRoot != null) {
137146
File newRoot = new File(fsRoot);
138147
if (newRoot.mkdirs() || newRoot.isDirectory()) {
139-
registerFilesystem(new LocalFilesystem(fsName, webView.getContext(), webView.getResourceApi(), newRoot));
148+
registerFilesystem(new LocalFilesystem(fsName, webView.getContext(), webView.getResourceApi(), newRoot, preferences));
140149
installedFileSystems.add(fsName);
141150
} else {
142-
LOG.d(LOG_TAG, "Unable to create root dir for filesystem \"" + fsName + "\", skipping");
151+
LOG.d(LOG_TAG, "Unable to create root dir for filesystem \"" + fsName + "\", skipping");
143152
}
144153
} else {
145154
LOG.d(LOG_TAG, "Unrecognized extra filesystem identifier: " + fsName);
@@ -217,10 +226,10 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
217226
// Note: The temporary and persistent filesystems need to be the first two
218227
// registered, so that they will match window.TEMPORARY and window.PERSISTENT,
219228
// per spec.
220-
this.registerFilesystem(new LocalFilesystem("temporary", webView.getContext(), webView.getResourceApi(), tmpRootFile));
221-
this.registerFilesystem(new LocalFilesystem("persistent", webView.getContext(), webView.getResourceApi(), persistentRootFile));
222-
this.registerFilesystem(new ContentFilesystem(webView.getContext(), webView.getResourceApi()));
223-
this.registerFilesystem(new AssetFilesystem(webView.getContext().getAssets(), webView.getResourceApi()));
229+
this.registerFilesystem(new LocalFilesystem("temporary", webView.getContext(), webView.getResourceApi(), tmpRootFile, preferences));
230+
this.registerFilesystem(new LocalFilesystem("persistent", webView.getContext(), webView.getResourceApi(), persistentRootFile, preferences));
231+
this.registerFilesystem(new ContentFilesystem(webView.getContext(), webView.getResourceApi(), preferences));
232+
this.registerFilesystem(new AssetFilesystem(webView.getContext().getAssets(), webView.getResourceApi(), preferences));
224233

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

@@ -249,13 +258,15 @@ public Uri remapUri(Uri uri) {
249258
if (!LocalFilesystemURL.FILESYSTEM_PROTOCOL.equals(uri.getScheme())) {
250259
return null;
251260
}
261+
252262
try {
253263
LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri);
254264
Filesystem fs = this.filesystemForURL(inputURL);
255265
if (fs == null) {
256266
return null;
257267
}
258268
String path = fs.filesystemPathForURL(inputURL);
269+
259270
if (path != null) {
260271
return Uri.parse("file://" + fs.filesystemPathForURL(inputURL));
261272
}
@@ -270,6 +281,7 @@ public boolean execute(String action, final String rawArgs, final CallbackContex
270281
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"));
271282
return true;
272283
}
284+
273285
if (action.equals("testSaveLocationExists")) {
274286
threadhelper(new FileOp() {
275287
public void run(JSONArray args) {
@@ -459,18 +471,24 @@ else if (action.equals("getFile")) {
459471
public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
460472
String dirname = args.getString(0);
461473
String path = args.getString(1);
462-
String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
463-
boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);
464474

465-
if(containsCreate && needPermission(nativeURL, WRITE)) {
466-
getWritePermission(rawArgs, ACTION_GET_FILE, callbackContext);
467-
}
468-
else if(!containsCreate && needPermission(nativeURL, READ)) {
469-
getReadPermission(rawArgs, ACTION_GET_FILE, callbackContext);
470-
}
471-
else {
475+
if (dirname.contains(LocalFilesystemURL.CDVFILE_KEYWORD) == true) {
472476
JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
473477
callbackContext.success(obj);
478+
} else {
479+
String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
480+
boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);
481+
482+
if(containsCreate && needPermission(nativeURL, WRITE)) {
483+
getWritePermission(rawArgs, ACTION_GET_FILE, callbackContext);
484+
}
485+
else if(!containsCreate && needPermission(nativeURL, READ)) {
486+
getReadPermission(rawArgs, ACTION_GET_FILE, callbackContext);
487+
}
488+
else {
489+
JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
490+
callbackContext.success(obj);
491+
}
474492
}
475493
}
476494
}, rawArgs, callbackContext);
@@ -878,6 +896,7 @@ private boolean remove(String baseURLstr) throws NoModificationAllowedException,
878896
private JSONObject getFile(String baseURLstr, String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
879897
try {
880898
LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
899+
881900
Filesystem fs = this.filesystemForURL(inputURL);
882901
if (fs == null) {
883902
throw new MalformedURLException("No installed handlers for this URL");
@@ -1222,4 +1241,68 @@ public void run(JSONArray args) throws JSONException, FileNotFoundException, IOE
12221241
LOG.d(LOG_TAG, "Received permission callback for unknown request code");
12231242
}
12241243
}
1244+
1245+
private String getMimeType(Uri uri) {
1246+
String fileExtensionFromUrl = MimeTypeMap.getFileExtensionFromUrl(uri.toString()).toLowerCase();
1247+
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtensionFromUrl);
1248+
}
1249+
1250+
public CordovaPluginPathHandler getPathHandler() {
1251+
WebViewAssetLoader.PathHandler pathHandler = path -> {
1252+
String targetFileSystem = null;
1253+
1254+
if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("persistent"))) {
1255+
targetFileSystem = "persistent";
1256+
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("temporary"))) {
1257+
targetFileSystem = "temporary";
1258+
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("files"))) {
1259+
targetFileSystem = "files";
1260+
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("documents"))) {
1261+
targetFileSystem = "documents";
1262+
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("cache"))) {
1263+
targetFileSystem = "cache";
1264+
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("root"))) {
1265+
targetFileSystem = "root";
1266+
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("files-external"))) {
1267+
targetFileSystem = "files-external";
1268+
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("sdcard"))) {
1269+
targetFileSystem = "sdcard";
1270+
} else if (path.startsWith(LocalFilesystemURL.fsNameToCdvKeyword("cache-external"))) {
1271+
targetFileSystem = "cache-external";
1272+
}
1273+
1274+
if (targetFileSystem != null) {
1275+
// Loop the registered file systems to find the target.
1276+
for (Filesystem fileSystem : filesystems) {
1277+
1278+
/*
1279+
* When target is discovered:
1280+
* 1. Transform the url path to the native path
1281+
* 2. Load the file contents
1282+
* 3. Get the file mime type
1283+
* 4. Return the file & mime information back we Web Resources
1284+
*/
1285+
if (fileSystem.name.equals(targetFileSystem)) {
1286+
// E.g. replace __cdvfile_persistent__ with native path "/data/user/0/com.example.file/files/files/"
1287+
String fileSystemNativeUri = fileSystem.rootUri.toString().replace("file://", "");
1288+
String fileTarget = path.replace(LocalFilesystemURL.fsNameToCdvKeyword(targetFileSystem) + "/", fileSystemNativeUri);
1289+
1290+
File file = new File(fileTarget);
1291+
1292+
try {
1293+
InputStream in = new FileInputStream(file);
1294+
String mimeType = getMimeType(Uri.parse(file.toString()));
1295+
return new WebResourceResponse(mimeType, null, in);
1296+
} catch (FileNotFoundException e) {
1297+
Log.e(LOG_TAG, e.getMessage());
1298+
}
1299+
}
1300+
}
1301+
}
1302+
1303+
return null;
1304+
};
1305+
1306+
return new CordovaPluginPathHandler(pathHandler);
1307+
}
12251308
}

src/android/Filesystem.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Licensed to the Apache Software Foundation (ASF) under one
2929
import java.util.ArrayList;
3030
import java.util.Arrays;
3131

32+
import org.apache.cordova.CordovaPreferences;
3233
import org.apache.cordova.CordovaResourceApi;
3334
import org.json.JSONArray;
3435
import org.json.JSONException;
@@ -38,13 +39,18 @@ public abstract class Filesystem {
3839

3940
protected final Uri rootUri;
4041
protected final CordovaResourceApi resourceApi;
42+
protected final CordovaPreferences preferences;
4143
public final String name;
4244
private JSONObject rootEntry;
4345

44-
public Filesystem(Uri rootUri, String name, CordovaResourceApi resourceApi) {
46+
static String SCHEME_HTTPS = "https";
47+
static String DEFAULT_HOSTNAME = "localhost";
48+
49+
public Filesystem(Uri rootUri, String name, CordovaResourceApi resourceApi, CordovaPreferences preferences) {
4550
this.rootUri = rootUri;
4651
this.name = name;
4752
this.resourceApi = resourceApi;
53+
this.preferences = preferences;
4854
}
4955

5056
public interface ReadFileCallback {
@@ -328,4 +334,15 @@ public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException
328334
return numBytesRead;
329335
}
330336
}
337+
338+
protected Uri.Builder createLocalUriBuilder() {
339+
String scheme = preferences.getString("scheme", SCHEME_HTTPS).toLowerCase();
340+
String hostname = preferences.getString("hostname", DEFAULT_HOSTNAME).toLowerCase();
341+
String path = LocalFilesystemURL.fsNameToCdvKeyword(name);
342+
343+
return new Uri.Builder()
344+
.scheme(scheme)
345+
.authority(hostname)
346+
.path(path);
347+
}
331348
}

0 commit comments

Comments
 (0)