From 02970566386920ff2ef2d9d8f82449ba5663eb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A8=E3=83=AA=E3=82=B9?= Date: Tue, 15 Mar 2022 16:21:09 +0900 Subject: [PATCH] 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 --- README.md | 15 ++++ package.json | 2 +- plugin.xml | 5 +- src/android/AssetFilesystem.java | 12 +-- src/android/ContentFilesystem.java | 14 ++-- src/android/FileUtils.java | 117 ++++++++++++++++++++++++---- src/android/Filesystem.java | 19 ++++- src/android/LocalFilesystem.java | 13 ++-- src/android/LocalFilesystemURL.java | 12 ++- www/Entry.js | 11 +-- www/android/FileSystem.js | 4 +- 11 files changed, 174 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 434ac7812..ae47fa3a9 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/package.json b/package.json index 74dddddc0..ebdc5e1f7 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/plugin.xml b/plugin.xml index 5716032d9..64eb7ec27 100644 --- a/plugin.xml +++ b/plugin.xml @@ -30,7 +30,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" https://github.com/apache/cordova-plugin-file/issues - + @@ -153,6 +153,9 @@ to config.xml in order for the application to find previously stored files. + + + diff --git a/src/android/AssetFilesystem.java b/src/android/AssetFilesystem.java index b035c40e6..6d766a4ad 100644 --- a/src/android/AssetFilesystem.java +++ b/src/android/AssetFilesystem.java @@ -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; @@ -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; } @@ -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); } diff --git a/src/android/ContentFilesystem.java b/src/android/ContentFilesystem.java index 6b983c089..f4df440de 100644 --- a/src/android/ContentFilesystem.java +++ b/src/android/ContentFilesystem.java @@ -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; @@ -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; } @@ -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); } diff --git a/src/android/FileUtils.java b/src/android/FileUtils.java index 695af7a59..cd2a3383b 100644 --- a/src/android/FileUtils.java +++ b/src/android/FileUtils.java @@ -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; @@ -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; @@ -87,8 +98,6 @@ public class FileUtils extends CordovaPlugin { private PendingRequests pendingRequests; - - /* * We need both read and write when accessing the storage, I think. */ @@ -136,10 +145,10 @@ protected void registerExtraFileSystems(String[] filesystems, HashMap { + 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); + } } diff --git a/src/android/Filesystem.java b/src/android/Filesystem.java index c69d3bdd0..54532211b 100644 --- a/src/android/Filesystem.java +++ b/src/android/Filesystem.java @@ -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; @@ -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 { @@ -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); + } } diff --git a/src/android/LocalFilesystem.java b/src/android/LocalFilesystem.java index 393344f4f..97e488824 100644 --- a/src/android/LocalFilesystem.java +++ b/src/android/LocalFilesystem.java @@ -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; @@ -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; } @@ -88,10 +90,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); } diff --git a/src/android/LocalFilesystemURL.java b/src/android/LocalFilesystemURL.java index b96b6ee49..d3f41d07a 100644 --- a/src/android/LocalFilesystemURL.java +++ b/src/android/LocalFilesystemURL.java @@ -23,6 +23,7 @@ Licensed to the Apache Software Foundation (ASF) under one public class LocalFilesystemURL { public static final String FILESYSTEM_PROTOCOL = "cdvfile"; + public static final String CDVFILE_KEYWORD = "__cdvfile_"; public final Uri uri; public final String fsName; @@ -37,19 +38,26 @@ private LocalFilesystemURL(Uri uri, String fsName, String fsPath, boolean isDire } public static LocalFilesystemURL parse(Uri uri) { - if (!FILESYSTEM_PROTOCOL.equals(uri.getScheme())) { + if(!uri.toString().contains(CDVFILE_KEYWORD)) { return null; } + String path = uri.getPath(); if (path.length() < 1) { return null; } + int firstSlashIdx = path.indexOf('/', 1); if (firstSlashIdx < 0) { return null; } + String fsName = path.substring(1, firstSlashIdx); + fsName = fsName.substring(CDVFILE_KEYWORD.length()); + fsName = fsName.substring(0, fsName.length() - 2); + path = path.substring(firstSlashIdx); + boolean isDirectory = path.charAt(path.length() - 1) == '/'; return new LocalFilesystemURL(uri, fsName, path, isDirectory); } @@ -58,6 +66,8 @@ public static LocalFilesystemURL parse(String uri) { return parse(Uri.parse(uri)); } + public static String fsNameToCdvKeyword(String fsName) { return CDVFILE_KEYWORD + fsName + "__"; } + public String toString() { return uri.toString(); } diff --git a/www/Entry.js b/www/Entry.js index a67be96fa..c90353765 100644 --- a/www/Entry.js +++ b/www/Entry.js @@ -187,15 +187,12 @@ Entry.prototype.toInternalURL = function () { /** * Return a URL that can be used to identify this entry. * Use a URL that can be used to as the src attribute of a