Skip to content

Commit

Permalink
feat(Android): Implement ionic-file and ionic-content urls (#242)
Browse files Browse the repository at this point in the history
* feat(Android): Implement ionic-file and ionic-content urls

closes #204, closes #183

* Change scheme and remove port
  • Loading branch information
jcesarmobile committed Dec 17, 2018
1 parent 486d412 commit 8ef0c30
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 159 deletions.
2 changes: 1 addition & 1 deletion plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

<platform name="android">
<config-file target="config.xml" parent="/*">
<allow-navigation href="http://localhost:8080/*"/>
<allow-navigation href="http://localhost/*"/>
<preference name="webView" value="com.ionicframework.cordova.webview.IonicWebViewEngine"/>
<feature name="IonicWebView">
<param name="android-package" value="com.ionicframework.cordova.webview.IonicWebView"/>
Expand Down
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(WebViewLocalServer.ionicContentScheme + ":///", "content://")));
} 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 @@ -59,11 +59,11 @@ public void init(CordovaWebView parentWebView, CordovaInterface cordova, final C
ConfigXmlParser parser = new ConfigXmlParser();
parser.parse(cordova.getActivity());

String port = preferences.getString("WKPort", "8080");
CDV_LOCAL_SERVER = "http://localhost:" + port;
String hostname = preferences.getString("Hostname", "localhost");
CDV_LOCAL_SERVER = "http://" + hostname;

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

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

Expand Down Expand Up @@ -143,7 +143,9 @@ public void onPageStarted(WebView view, String url, Bitmap favicon) {
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
view.loadUrl("javascript:(function() { " +
"window.WEBVIEW_SERVER_URL = '" + CDV_LOCAL_SERVER + "'" +
"window.WEBVIEW_SERVER_URL = '" + CDV_LOCAL_SERVER + "';" +
"window.WEBVIEW_FILE_PREFIX = '" + WebViewLocalServer.ionicFileScheme + "';" +
"window.WEBVIEW_CONTENT_PREFIX = '" + WebViewLocalServer.ionicContentScheme + "';" +
"})()");
}
}
Expand Down
193 changes: 61 additions & 132 deletions src/android/com/ionicframework/cordova/webview/WebViewLocalServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,16 @@
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";
public final static String ionicFileScheme = "app-file";
public final static String ionicContentScheme = "app-content";

private final UriMatcher uriMatcher;
private final AndroidProtocolHandler protocolHandler;
private final String authority;
// Whether we're serving local files or proxying (for example, when doing livereload on a
// non-local endpoint (will be false in that case)
private final boolean isLocal;
private boolean isAsset;
// Whether to route all requests to paths without extensions back to `index.html`
private final boolean html5mode;
Expand Down Expand Up @@ -169,18 +166,7 @@ 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.isLocal = true;
this.authority = UUID.randomUUID().toString() + "" + knownUnusedAuthority;
}
this.authority = authority;
}

private static Uri parseAndVerifyUrl(String url) {
Expand Down Expand Up @@ -234,24 +220,38 @@ public WebResourceResponse shouldInterceptRequest(Uri uri) {
return null;
}

if (this.isLocal) {
if (isLocalFile(uri) || uri.getAuthority().equals(this.authority)) {
Log.d("SERVER", "Handling local request: " + uri.toString());
return handleLocalRequest(uri, handler);
} else {
return handleProxyRequest(uri, handler);
}
}

private boolean isLocalFile(Uri uri) {
if (uri.getScheme().equals(ionicContentScheme) || uri.getScheme().equals(ionicFileScheme)) {
return true;
}
return false;
}
private WebResourceResponse handleLocalRequest(Uri uri, PathHandler handler) {
String path = uri.getPath();

if (isLocalFile(uri)) {
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', window.WEBVIEW_FILE_PREFIX);
}
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://', window.WEBVIEW_CONTENT_PREFIX + ':///');
}
return window.WEBVIEW_SERVER_URL + '/_file_' + url;

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

0 comments on commit 8ef0c30

Please sign in to comment.