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): Implement ionic-file and ionic-content urls #242

Merged
merged 2 commits into from
Dec 17, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,8 @@ public AndroidProtocolHandler(Context context) {
this.context = context;
}

public InputStream openAsset(String path, String assetPath) throws IOException {
if (path.startsWith(assetPath + "/_file_")) {
if (path.contains("content://")) {
String contentPath = path.replace(assetPath + "/_file_/", "content://");
return context.getContentResolver().openInputStream(Uri.parse(contentPath));
} else {
String filePath = path.replace(assetPath + "/_file_/", "");
return new FileInputStream(new File(filePath));
}
} else {
return context.getAssets().open(path, AssetManager.ACCESS_STREAMING);
}
public InputStream openAsset(String path) throws IOException {
return context.getAssets().open(path, AssetManager.ACCESS_STREAMING);
}

public InputStream openResource(Uri uri) {
Expand Down Expand Up @@ -82,6 +72,16 @@ public InputStream openFile(String filePath) throws IOException {
return new FileInputStream(localFile);
}

public InputStream openContentUrl(Uri uri) throws IOException {
InputStream stream = null;
try {
stream = context.getContentResolver().openInputStream(Uri.parse(uri.toString().replace("ionic-content:///", "content://")));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure ionic-content is the right sheme here? Technically this is just a generic webview plugin and isn't ionic related. Would be the same question for Capacitor. How about app-content://?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to app-content and app-file, but as it's "Ionic WebView" I think it makes sense to use ionic as prefix on the schemes.
This schemes are only understood by the WebView, and we provide the logic to convert the file/content urls to something the WebView understands.

Specially in Capacitor I think it makes sense to have them like capacitor-*

} catch (SecurityException e) {
Log.e(TAG, "Unable to open content URL: " + uri, e);
}
return stream;
}

private static int getFieldId(Context context, String assetType, String assetName)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Class<?> d = context.getClassLoader()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void init(CordovaWebView parentWebView, CordovaInterface cordova, final C
CDV_LOCAL_SERVER = "http://localhost:" + port;

localServer = new WebViewLocalServer(cordova.getActivity(), "localhost:" + port, true, parser);
WebViewLocalServer.AssetHostingDetails ahd = localServer.hostAssets("www");
localServer.hostAssets("www");

webView.setWebViewClient(new ServerClient(this, parser));

Expand Down
185 changes: 57 additions & 128 deletions src/android/com/ionicframework/cordova/webview/WebViewLocalServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,10 @@
public class WebViewLocalServer {
private static String TAG = "WebViewAssetServer";
private String basePath;
/**
* capacitorapp.net is reserved by the Ionic team for use in local capacitor apps.
*/
public final static String knownUnusedAuthority = "capacitorapp.net";
private final static String httpScheme = "http";
private final static String httpsScheme = "https";
private final static String ionicFileScheme = "ionic-file";
private final static String ionicContentScheme = "ionic-content";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above re: ionic-content and ionic-file


private final UriMatcher uriMatcher;
private final AndroidProtocolHandler protocolHandler;
Expand Down Expand Up @@ -169,17 +167,11 @@ public Uri getHttpsPrefix() {
this.html5mode = html5mode;
this.parser = parser;
this.protocolHandler = new AndroidProtocolHandler(context.getApplicationContext());
if (authority != null) {
this.authority = authority;
if (authority.startsWith("localhost")) {
this.isLocal = true;
} else {
this.isLocal = false;
}

} else {
this.authority = authority;
if (authority.startsWith("localhost")) {
this.isLocal = true;
this.authority = UUID.randomUUID().toString() + "" + knownUnusedAuthority;
} else {
this.isLocal = false;
}
}

Expand Down Expand Up @@ -244,14 +236,22 @@ public WebResourceResponse shouldInterceptRequest(Uri uri) {

private WebResourceResponse handleLocalRequest(Uri uri, PathHandler handler) {
String path = uri.getPath();

if (uri.getScheme().equals(ionicContentScheme) || uri.getScheme().equals(ionicFileScheme)) {
InputStream responseStream = new LollipopLazyInputStream(handler, uri);
String mimeType = getMimeType(path, responseStream);
return createWebResourceResponse(mimeType, handler.getEncoding(),
handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), responseStream);
}

if (path.equals("/") || (!uri.getLastPathSegment().contains(".") && html5mode)) {
InputStream stream;
String launchURL = parser.getLaunchUrl();
String launchFile = launchURL.substring(launchURL.lastIndexOf("/") + 1, launchURL.length());
try {
String startPath = this.basePath + "/" + launchFile;
if (isAsset) {
stream = protocolHandler.openAsset(startPath, "");
stream = protocolHandler.openAsset(startPath);
} else {
stream = protocolHandler.openFile(startPath);
}
Expand All @@ -267,15 +267,10 @@ private WebResourceResponse handleLocalRequest(Uri uri, PathHandler handler) {

int periodIndex = path.lastIndexOf(".");
if (periodIndex >= 0) {
String ext = path.substring(path.lastIndexOf("."), path.length());

InputStream responseStream = new LollipopLazyInputStream(handler, uri);
InputStream stream = responseStream;

String mimeType = getMimeType(path, stream);

String mimeType = getMimeType(path, responseStream);
return createWebResourceResponse(mimeType, handler.getEncoding(),
handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream);
handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), responseStream);
}

return null;
Expand Down Expand Up @@ -375,31 +370,12 @@ void register(Uri uri, PathHandler handler) {
*
* @param assetPath the local path in the application's asset folder which will be made
* available by the server (for example "/www").
* @return prefixes under which the assets are hosted.
*/
public AssetHostingDetails hostAssets(String assetPath) {
return hostAssets(authority, assetPath, "", true, true);
public void hostAssets(String assetPath) {
hostAssets(authority, assetPath);
}


/**
* Hosts the application's assets on an http(s):// URL. Assets from the local path
* <code>assetPath/...</code> will be available under
* <code>http(s)://{uuid}.androidplatform.net/{virtualAssetPath}/...</code>.
*
* @param assetPath the local path in the application's asset folder which will be made
* available by the server (for example "/www").
* @param virtualAssetPath the path on the local server under which the assets should be hosted.
* @param enableHttp whether to enable hosting using the http scheme.
* @param enableHttps whether to enable hosting using the https scheme.
* @return prefixes under which the assets are hosted.
*/
public AssetHostingDetails hostAssets(final String assetPath, final String virtualAssetPath,
boolean enableHttp, boolean enableHttps) {
return hostAssets(authority, assetPath, virtualAssetPath, enableHttp,
enableHttps);
}

/**
* Hosts the application's assets on an http(s):// URL. Assets from the local path
* <code>assetPath/...</code> will be available under
Expand All @@ -408,39 +384,39 @@ public AssetHostingDetails hostAssets(final String assetPath, final String virtu
* @param domain custom domain on which the assets should be hosted (for example "example.com").
* @param assetPath the local path in the application's asset folder which will be made
* available by the server (for example "/www").
* @param virtualAssetPath the path on the local server under which the assets should be hosted.
* @param enableHttp whether to enable hosting using the http scheme.
* @param enableHttps whether to enable hosting using the https scheme.
* @return prefixes under which the assets are hosted.
*/
public AssetHostingDetails hostAssets(final String domain,
final String assetPath, final String virtualAssetPath,
boolean enableHttp, boolean enableHttps) {
public void hostAssets(final String domain,
final String assetPath) {
this.isAsset = true;
this.basePath = assetPath;
Uri.Builder uriBuilder = new Uri.Builder();
uriBuilder.scheme(httpScheme);
uriBuilder.authority(domain);
uriBuilder.path(virtualAssetPath);

createHostingDetails();
}

private void createHostingDetails() {
final String assetPath = this.basePath;

if (assetPath.indexOf('*') != -1) {
throw new IllegalArgumentException("assetPath cannot contain the '*' character.");
}
if (virtualAssetPath.indexOf('*') != -1) {
throw new IllegalArgumentException(
"virtualAssetPath cannot contain the '*' character.");
}

Uri httpPrefix = null;
Uri httpsPrefix = null;

PathHandler handler = new PathHandler() {
@Override
public InputStream handle(Uri url) {
InputStream stream;
String path = url.getPath().replaceFirst(virtualAssetPath, assetPath);
InputStream stream = null;
String path = url.getPath();
if (!isAsset) {
path = basePath + url.getPath();
}
try {
stream = protocolHandler.openAsset(path, assetPath);
if ((url.getScheme().equals(httpScheme) || url.getScheme().equals(httpsScheme))&& isAsset) {
stream = protocolHandler.openAsset(assetPath + path);
} else if (url.getScheme().equals(ionicFileScheme) || !isAsset) {
stream = protocolHandler.openFile(path);
} else if (url.getScheme().equals(ionicContentScheme)) {
stream = protocolHandler.openContentUrl(url);
}
} catch (IOException e) {
Log.e(TAG, "Unable to open asset URL: " + url);
return null;
Expand All @@ -450,18 +426,22 @@ public InputStream handle(Uri url) {
}
};

if (enableHttp) {
httpPrefix = uriBuilder.build();
register(Uri.withAppendedPath(httpPrefix, "/"), handler);
register(Uri.withAppendedPath(httpPrefix, "**"), handler);
}
if (enableHttps) {
uriBuilder.scheme(httpsScheme);
httpsPrefix = uriBuilder.build();
register(Uri.withAppendedPath(httpsPrefix, "/"), handler);
register(Uri.withAppendedPath(httpsPrefix, "**"), handler);
}
return new AssetHostingDetails(httpPrefix, httpsPrefix);
registerUriForScheme(httpScheme, handler, authority);
registerUriForScheme(httpsScheme, handler, authority);
registerUriForScheme(ionicFileScheme, handler, "");
registerUriForScheme(ionicContentScheme, handler, "");

}

private void registerUriForScheme(String scheme, PathHandler handler, String authority) {
Uri.Builder uriBuilder = new Uri.Builder();
uriBuilder.scheme(scheme);
uriBuilder.authority(authority);
uriBuilder.path("");
Uri uriPrefix = uriBuilder.build();

register(Uri.withAppendedPath(uriPrefix, "/"), handler);
register(Uri.withAppendedPath(uriPrefix, "**"), handler);
}

/**
Expand Down Expand Up @@ -553,62 +533,11 @@ public InputStream handle(Uri url) {
*
* @param basePath the local path in the application's data folder which will be made
* available by the server (for example "/www").
* @return prefixes under which the assets are hosted.
*/
public AssetHostingDetails hostFiles(String basePath) {
return hostFiles(basePath, true, true);
}

public AssetHostingDetails hostFiles(final String basePath, boolean enableHttp,
boolean enableHttps) {
public void hostFiles(final String basePath) {
this.isAsset = false;
this.basePath = basePath;
Uri.Builder uriBuilder = new Uri.Builder();
uriBuilder.scheme(httpScheme);
uriBuilder.authority(authority);
uriBuilder.path("");

Uri httpPrefix = null;
Uri httpsPrefix = null;

PathHandler handler = new PathHandler() {
@Override
public InputStream handle(Uri url) {
InputStream stream;
try {
if (url.getPath().startsWith("/_file_/")) {
stream = protocolHandler.openFile( url.getPath().replace("/_file_/", ""));
} else {
stream = protocolHandler.openFile(basePath + url.getPath());
}
} catch (IOException e) {
Log.e(TAG, "Unable to open asset URL: " + url);
return null;
}

String mimeType = null;
try {
mimeType = URLConnection.guessContentTypeFromStream(stream);
} catch (Exception ex) {
Log.e(TAG, "Unable to get mime type" + url);
}

return stream;
}
};

if (enableHttp) {
httpPrefix = uriBuilder.build();
register(Uri.withAppendedPath(httpPrefix, "/"), handler);
register(Uri.withAppendedPath(httpPrefix, "**"), handler);
}
if (enableHttps) {
uriBuilder.scheme(httpsScheme);
httpsPrefix = uriBuilder.build();
register(Uri.withAppendedPath(httpsPrefix, "/"), handler);
register(Uri.withAppendedPath(httpsPrefix, "**"), handler);
}
return new AssetHostingDetails(httpPrefix, httpsPrefix);
createHostingDetails();
}

/**
Expand Down
15 changes: 6 additions & 9 deletions src/www/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,14 @@ var WebView = {
if (!url) {
return url;
}
if (!url.startsWith('file://')) {
return url;
}
if (window.WEBVIEW_SERVER_URL.startsWith('ionic://')) {
return url.replace('file', 'ionic-asset');
if (url.startsWith('file://')) {
return url.replace('file', 'ionic-file');;
}
url = url.substr(7); // len("file://") == 7
if (url.length === 0 || url[0] !== '/') { // ensure the new URL starts with /
url = '/' + url;
if (url.startsWith('content://')) {
return url.replace('content://', 'ionic-content:///');;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watch for hard coded strings here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed on last commit

}
return window.WEBVIEW_SERVER_URL + '/_file_' + url;

return url;
},
setServerBasePath: function(path) {
exec(null, null, 'IonicWebView', 'setServerBasePath', [path]);
Expand Down