From ea6631a89108e0ececbee4a80b13d3aa5ec0ca28 Mon Sep 17 00:00:00 2001 From: Niklas Merz Date: Fri, 27 Nov 2020 19:14:32 +0100 Subject: [PATCH 1/4] (android): replace web server with assetloader See #483 --- plugin.xml | 2 - .../webview/AndroidProtocolHandler.java | 6 +- .../cordova/webview/IonicWebViewEngine.java | 262 +++---- .../cordova/webview/UriMatcher.java | 183 ----- .../cordova/webview/WebViewLocalServer.java | 645 ------------------ 5 files changed, 135 insertions(+), 963 deletions(-) delete mode 100644 src/android/com/ionicframework/cordova/webview/UriMatcher.java delete mode 100644 src/android/com/ionicframework/cordova/webview/WebViewLocalServer.java diff --git a/plugin.xml b/plugin.xml index 846e12a7..e9c3bf69 100644 --- a/plugin.xml +++ b/plugin.xml @@ -50,8 +50,6 @@ - - diff --git a/src/android/com/ionicframework/cordova/webview/AndroidProtocolHandler.java b/src/android/com/ionicframework/cordova/webview/AndroidProtocolHandler.java index 6202a5b9..a4cbc296 100644 --- a/src/android/com/ionicframework/cordova/webview/AndroidProtocolHandler.java +++ b/src/android/com/ionicframework/cordova/webview/AndroidProtocolHandler.java @@ -68,7 +68,7 @@ public InputStream openResource(Uri uri) { } public InputStream openFile(String filePath) throws IOException { - String realPath = filePath.replace(WebViewLocalServer.fileStart, ""); + String realPath = filePath; File localFile = new File(realPath); return new FileInputStream(localFile); } @@ -77,9 +77,9 @@ public InputStream openContentUrl(Uri uri) throws IOException { Integer port = uri.getPort(); String realPath; if (port == -1) { - realPath = uri.toString().replace(uri.getScheme() + "://" + uri.getHost() + WebViewLocalServer.contentStart, "content:/"); + realPath = uri.toString().replace(uri.getScheme() + "://" + uri.getHost(), "content:/"); } else { - realPath = uri.toString().replace(uri.getScheme() + "://" + uri.getHost() + ":" + port + WebViewLocalServer.contentStart, "content:/"); + realPath = uri.toString().replace(uri.getScheme() + "://" + uri.getHost() + ":" + port, "content:/"); } InputStream stream = null; try { diff --git a/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java b/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java index 7918435f..85a29a7d 100644 --- a/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java +++ b/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java @@ -1,11 +1,10 @@ package com.ionicframework.cordova.webview; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; import android.graphics.Bitmap; -import android.net.Uri; import android.os.Build; import android.util.Log; import android.webkit.ServiceWorkerController; @@ -14,6 +13,7 @@ import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; + import org.apache.cordova.ConfigXmlParser; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPreferences; @@ -26,147 +26,149 @@ import org.apache.cordova.engine.SystemWebViewEngine; import org.apache.cordova.engine.SystemWebView; -public class IonicWebViewEngine extends SystemWebViewEngine { - public static final String TAG = "IonicWebViewEngine"; - - private WebViewLocalServer localServer; - private String CDV_LOCAL_SERVER; - private String scheme; - private static final String LAST_BINARY_VERSION_CODE = "lastBinaryVersionCode"; - private static final String LAST_BINARY_VERSION_NAME = "lastBinaryVersionName"; - - /** - * Used when created via reflection. - */ - public IonicWebViewEngine(Context context, CordovaPreferences preferences) { - super(new SystemWebView(context), preferences); - Log.d(TAG, "Ionic Web View Engine Starting Right Up 1..."); - } - - public IonicWebViewEngine(SystemWebView webView) { - super(webView, null); - Log.d(TAG, "Ionic Web View Engine Starting Right Up 2..."); - } - - public IonicWebViewEngine(SystemWebView webView, CordovaPreferences preferences) { - super(webView, preferences); - Log.d(TAG, "Ionic Web View Engine Starting Right Up 3..."); - } - - @Override - public void init(CordovaWebView parentWebView, CordovaInterface cordova, final CordovaWebViewEngine.Client client, - CordovaResourceApi resourceApi, PluginManager pluginManager, - NativeToJsMessageQueue nativeToJsMessageQueue) { - ConfigXmlParser parser = new ConfigXmlParser(); - parser.parse(cordova.getActivity()); - - String hostname = preferences.getString("Hostname", "localhost"); - scheme = preferences.getString("Scheme", "http"); - CDV_LOCAL_SERVER = scheme + "://" + hostname; - - localServer = new WebViewLocalServer(cordova.getActivity(), hostname, true, parser, scheme); - localServer.hostAssets("www"); - - webView.setWebViewClient(new ServerClient(this, parser)); - - super.init(parentWebView, cordova, client, resourceApi, pluginManager, nativeToJsMessageQueue); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - final WebSettings settings = webView.getSettings(); - int mode = preferences.getInteger("MixedContentMode", 0); - settings.setMixedContentMode(mode); - } - SharedPreferences prefs = cordova.getActivity().getApplicationContext().getSharedPreferences(IonicWebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE); - String path = prefs.getString(IonicWebView.CDV_SERVER_PATH, null); - if (!isDeployDisabled() && !isNewBinary() && path != null && !path.isEmpty()) { - setServerBasePath(path); - } +import androidx.webkit.WebViewAssetLoader; +import androidx.webkit.internal.AssetHelper; - boolean setAsServiceWorkerClient = preferences.getBoolean("ResolveServiceWorkerRequests", false); - ServiceWorkerController controller = null; +import java.io.InputStream; - if (setAsServiceWorkerClient && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - controller = ServiceWorkerController.getInstance(); - controller.setServiceWorkerClient(new ServiceWorkerClient(){ - @Override - public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { - return localServer.shouldInterceptRequest(request.getUrl(), request); - } - }); - } - } - - private boolean isNewBinary() { - String versionCode = ""; - String versionName = ""; - SharedPreferences prefs = cordova.getActivity().getApplicationContext().getSharedPreferences(IonicWebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE); - String lastVersionCode = prefs.getString(LAST_BINARY_VERSION_CODE, null); - String lastVersionName = prefs.getString(LAST_BINARY_VERSION_NAME, null); - - try { - PackageInfo pInfo = this.cordova.getActivity().getPackageManager().getPackageInfo(this.cordova.getActivity().getPackageName(), 0); - versionCode = Integer.toString(pInfo.versionCode); - versionName = pInfo.versionName; - } catch(Exception ex) { - Log.e(TAG, "Unable to get package info", ex); +public class IonicWebViewEngine extends SystemWebViewEngine { + public static final String TAG = "IonicWebViewEngine"; + private String LOCAL_SERVER; + private String scheme; + + public final static String httpScheme = "http"; + public final static String httpsScheme = "https"; + + private WebViewAssetLoader assetLoader; + private AndroidProtocolHandler protocolHandler; + + /** + * Used when created via reflection. + */ + public IonicWebViewEngine(Context context, CordovaPreferences preferences) { + super(new SystemWebView(context), preferences); + Log.d(TAG, "Ionic Web View Engine Starting Right Up 1..."); } - if (!versionCode.equals(lastVersionCode) || !versionName.equals(lastVersionName)) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(LAST_BINARY_VERSION_CODE, versionCode); - editor.putString(LAST_BINARY_VERSION_NAME, versionName); - editor.putString(IonicWebView.CDV_SERVER_PATH, ""); - editor.apply(); - return true; - } - return false; - } - - private boolean isDeployDisabled() { - return preferences.getBoolean("DisableDeploy", false); - } - private class ServerClient extends SystemWebViewClient { - private ConfigXmlParser parser; - - public ServerClient(SystemWebViewEngine parentEngine, ConfigXmlParser parser) { - super(parentEngine); - this.parser = parser; + public IonicWebViewEngine(SystemWebView webView) { + super(webView, null); + Log.d(TAG, "Ionic Web View Engine Starting Right Up 2..."); } - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { - return localServer.shouldInterceptRequest(request.getUrl(), request); + public IonicWebViewEngine(SystemWebView webView, CordovaPreferences preferences) { + super(webView, preferences); + Log.d(TAG, "Ionic Web View Engine Starting Right Up 3..."); } @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - super.onPageStarted(view, url, favicon); - String launchUrl = parser.getLaunchUrl(); - if (!launchUrl.contains(WebViewLocalServer.httpsScheme) && !launchUrl.contains(WebViewLocalServer.httpScheme) && url.equals(launchUrl)) { - view.stopLoading(); - // When using a custom scheme the app won't load if server start url doesn't end in / - String startUrl = CDV_LOCAL_SERVER; - if (!scheme.equalsIgnoreCase(WebViewLocalServer.httpsScheme) && !scheme.equalsIgnoreCase(WebViewLocalServer.httpScheme)) { - startUrl += "/"; + public void init(CordovaWebView parentWebView, CordovaInterface cordova, final CordovaWebViewEngine.Client client, + CordovaResourceApi resourceApi, PluginManager pluginManager, + NativeToJsMessageQueue nativeToJsMessageQueue) { + ConfigXmlParser parser = new ConfigXmlParser(); + parser.parse(cordova.getActivity()); + + this.protocolHandler = new AndroidProtocolHandler(cordova.getActivity().getApplicationContext().getApplicationContext()); + + String hostname = preferences.getString("Hostname", "localhost"); + scheme = preferences.getString("Scheme", "https"); + LOCAL_SERVER = scheme + "://" + hostname; + + assetLoader = new WebViewAssetLoader.Builder() + .setDomain(hostname) + .setHttpAllowed(true) + // ---- Path handler + // Default path handler not working + // .addPathHandler("/assets/", new WebViewAssetLoader.AssetsPathHandler(this)) + // .addPathHandler("/res/", new WebViewAssetLoader.ResourcesPathHandler(this)) + // => implementing custom handler + .addPathHandler("/", path -> { + try { + if (path.isEmpty()) + path = "index.html"; + InputStream is = protocolHandler.openAsset("www/" + path); + @SuppressLint("RestrictedApi") String mimeType = AssetHelper.guessMimeType(path); + + return new WebResourceResponse(mimeType, null, is); + } catch (Exception e) { + e.printStackTrace(); + Log.e("WebViewAssetLoader", e.getMessage()); + } + return null; + }) + // ---- + .build(); + + webView.setWebViewClient(new ServerClient(this, parser)); + + super.init(parentWebView, cordova, client, resourceApi, pluginManager, nativeToJsMessageQueue); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final WebSettings settings = webView.getSettings(); + int mode = preferences.getInteger("MixedContentMode", 0); + settings.setMixedContentMode(mode); + } + SharedPreferences prefs = cordova.getActivity().getApplicationContext().getSharedPreferences(IonicWebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE); + String path = prefs.getString(IonicWebView.CDV_SERVER_PATH, null); + + + boolean setAsServiceWorkerClient = preferences.getBoolean("ResolveServiceWorkerRequests", false); + ServiceWorkerController controller = null; + + if (setAsServiceWorkerClient && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + controller = ServiceWorkerController.getInstance(); + controller.setServiceWorkerClient(new ServiceWorkerClient() { + @Override + public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { + return assetLoader.shouldInterceptRequest(request.getUrl()); + } + }); } - view.loadUrl(startUrl); - } } - @Override - public void onPageFinished(WebView view, String url) { - super.onPageFinished(view, url); - view.loadUrl("javascript:(function() { " + - "window.WEBVIEW_SERVER_URL = '" + CDV_LOCAL_SERVER + "';" + - "})()"); + private class ServerClient extends SystemWebViewClient { + private ConfigXmlParser parser; + + public ServerClient(SystemWebViewEngine parentEngine, ConfigXmlParser parser) { + super(parentEngine); + this.parser = parser; + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + WebResourceResponse intercept = assetLoader.shouldInterceptRequest(request.getUrl()); + return intercept; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + String launchUrl = parser.getLaunchUrl(); + if (!launchUrl.contains(IonicWebViewEngine.httpsScheme) && !launchUrl.contains(IonicWebViewEngine.httpScheme) && url.equals(launchUrl)) { + view.stopLoading(); + // When using a custom scheme the app won't load if server start url doesn't end in / + String startUrl = LOCAL_SERVER; + if (!scheme.equalsIgnoreCase(IonicWebViewEngine.httpsScheme) && !scheme.equalsIgnoreCase(IonicWebViewEngine.httpScheme)) { + startUrl += "/"; + } + view.loadUrl(startUrl); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + view.loadUrl("javascript:(function() { " + + "window.WEBVIEW_SERVER_URL = '" + LOCAL_SERVER + "';" + + "})()"); + } } - } - public void setServerBasePath(String path) { - localServer.hostFiles(path); - webView.loadUrl(CDV_LOCAL_SERVER); - } + public void setServerBasePath(String path) { + //localServer.hostFiles(path); + //ebView.loadUrl(CDV_LOCAL_SERVER); + } - public String getServerBasePath() { - return this.localServer.getBasePath(); - } + public String getServerBasePath() { + //return this.localServer.getBasePath(); + return ""; + } } diff --git a/src/android/com/ionicframework/cordova/webview/UriMatcher.java b/src/android/com/ionicframework/cordova/webview/UriMatcher.java deleted file mode 100644 index d8addb9b..00000000 --- a/src/android/com/ionicframework/cordova/webview/UriMatcher.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.ionicframework.cordova.webview; - -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -//package com.google.webviewlocalserver.third_party.android; - -import android.net.Uri; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -public class UriMatcher { - /** - * Creates the root node of the URI tree. - * - * @param code the code to match for the root URI - */ - public UriMatcher(Object code) { - mCode = code; - mWhich = -1; - mChildren = new ArrayList(); - mText = null; - } - - private UriMatcher() { - mCode = null; - mWhich = -1; - mChildren = new ArrayList(); - mText = null; - } - - /** - * Add a URI to match, and the code to return when this URI is - * matched. URI nodes may be exact match string, the token "*" - * that matches any text, or the token "#" that matches only - * numbers. - *

- * Starting from API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, - * this method will accept a leading slash in the path. - * - * @param authority the authority to match - * @param path the path to match. * may be used as a wild card for - * any text, and # may be used as a wild card for numbers. - * @param code the code that is returned when a URI is matched - * against the given components. Must be positive. - */ - public void addURI(String scheme, String authority, String path, Object code) { - if (code == null) { - throw new IllegalArgumentException("Code can't be null"); - } - - String[] tokens = null; - if (path != null) { - String newPath = path; - // Strip leading slash if present. - if (path.length() > 0 && path.charAt(0) == '/') { - newPath = path.substring(1); - } - tokens = PATH_SPLIT_PATTERN.split(newPath); - } - - int numTokens = tokens != null ? tokens.length : 0; - UriMatcher node = this; - for (int i = -2; i < numTokens; i++) { - String token; - if (i == -2) - token = scheme; - else if (i == -1) - token = authority; - else - token = tokens[i]; - ArrayList children = node.mChildren; - int numChildren = children.size(); - UriMatcher child; - int j; - for (j = 0; j < numChildren; j++) { - child = children.get(j); - if (token.equals(child.mText)) { - node = child; - break; - } - } - if (j == numChildren) { - // Child not found, create it - child = new UriMatcher(); - if (token.equals("**")) { - child.mWhich = REST; - } else if (token.equals("*")) { - child.mWhich = TEXT; - } else { - child.mWhich = EXACT; - } - child.mText = token; - node.mChildren.add(child); - node = child; - } - } - node.mCode = code; - } - - static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/"); - - /** - * Try to match against the path in a url. - * - * @param uri The url whose path we will match against. - * @return The code for the matched node (added using addURI), - * or null if there is no matched node. - */ - public Object match(Uri uri) { - final List pathSegments = uri.getPathSegments(); - final int li = pathSegments.size(); - - UriMatcher node = this; - - if (li == 0 && uri.getAuthority() == null) { - return this.mCode; - } - - for (int i = -2; i < li; i++) { - String u; - if (i == -2) - u = uri.getScheme(); - else if (i == -1) - u = uri.getAuthority(); - else - u = pathSegments.get(i); - ArrayList list = node.mChildren; - if (list == null) { - break; - } - node = null; - int lj = list.size(); - for (int j = 0; j < lj; j++) { - UriMatcher n = list.get(j); - which_switch: - switch (n.mWhich) { - case EXACT: - if (n.mText.equals(u)) { - node = n; - } - break; - case TEXT: - node = n; - break; - case REST: - return n.mCode; - } - if (node != null) { - break; - } - } - if (node == null) { - return null; - } - } - - return node.mCode; - } - - private static final int EXACT = 0; - private static final int TEXT = 1; - private static final int REST = 2; - - private Object mCode; - private int mWhich; - private String mText; - private ArrayList mChildren; -} diff --git a/src/android/com/ionicframework/cordova/webview/WebViewLocalServer.java b/src/android/com/ionicframework/cordova/webview/WebViewLocalServer.java deleted file mode 100644 index cf134932..00000000 --- a/src/android/com/ionicframework/cordova/webview/WebViewLocalServer.java +++ /dev/null @@ -1,645 +0,0 @@ -/* -Copyright 2015 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - */ -package com.ionicframework.cordova.webview; - -import android.content.Context; -import android.net.Uri; -import android.os.Build; -import android.util.Log; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; - -import org.apache.cordova.ConfigXmlParser; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.net.URLConnection; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -/** - * Helper class meant to be used with the android.webkit.WebView class to enable hosting assets, - * resources and other data on 'virtual' http(s):// URL. - * Hosting assets and resources on http(s):// URLs is desirable as it is compatible with the - * Same-Origin policy. - *

- * This class is intended to be used from within the - * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, String)} and - * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, - * android.webkit.WebResourceRequest)} - * methods. - */ -public class WebViewLocalServer { - private static String TAG = "WebViewAssetServer"; - private String basePath; - public final static String httpScheme = "http"; - public final static String httpsScheme = "https"; - public final static String fileStart = "/_app_file_"; - public final static String contentStart = "/_app_content_"; - - private final UriMatcher uriMatcher; - private final AndroidProtocolHandler protocolHandler; - private final String authority; - private final String customScheme; - // 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 boolean isAsset; - // Whether to route all requests to paths without extensions back to `index.html` - private final boolean html5mode; - private ConfigXmlParser parser; - - public String getAuthority() { return authority; } - - /** - * A handler that produces responses for paths on the virtual asset server. - *

- * Methods of this handler will be invoked on a background thread and care must be taken to - * correctly synchronize access to any shared state. - *

- * On Android KitKat and above these methods may be called on more than one thread. This thread - * may be different than the thread on which the shouldInterceptRequest method was invoke. - * This means that on Android KitKat and above it is possible to block in this method without - * blocking other resources from loading. The number of threads used to parallelize loading - * is an internal implementation detail of the WebView and may change between updates which - * means that the amount of time spend blocking in this method should be kept to an absolute - * minimum. - */ - public abstract static class PathHandler { - protected String mimeType; - private String encoding; - private String charset; - private int statusCode; - private String reasonPhrase; - private Map responseHeaders; - - public PathHandler() { - this(null, null, 200, "OK", null); - } - - public PathHandler(String encoding, String charset, int statusCode, - String reasonPhrase, Map responseHeaders) { - this.encoding = encoding; - this.charset = charset; - this.statusCode = statusCode; - this.reasonPhrase = reasonPhrase; - Map tempResponseHeaders; - if (responseHeaders == null) { - tempResponseHeaders = new HashMap(); - } else { - tempResponseHeaders = responseHeaders; - } - tempResponseHeaders.put("Cache-Control", "no-cache"); - this.responseHeaders = tempResponseHeaders; - } - - abstract public InputStream handle(Uri url); - - public String getEncoding() { - return encoding; - } - - public String getCharset() { - return charset; - } - - public int getStatusCode() { - return statusCode; - } - - public String getReasonPhrase() { - return reasonPhrase; - } - - public Map getResponseHeaders() { - return responseHeaders; - } - } - - /** - * Information about the URLs used to host the assets in the WebView. - */ - public static class AssetHostingDetails { - private Uri httpPrefix; - private Uri httpsPrefix; - - /*package*/ AssetHostingDetails(Uri httpPrefix, Uri httpsPrefix) { - this.httpPrefix = httpPrefix; - this.httpsPrefix = httpsPrefix; - } - - /** - * Gets the http: scheme prefix at which assets are hosted. - * - * @return the http: scheme prefix at which assets are hosted. Can return null. - */ - public Uri getHttpPrefix() { - return httpPrefix; - } - - /** - * Gets the https: scheme prefix at which assets are hosted. - * - * @return the https: scheme prefix at which assets are hosted. Can return null. - */ - public Uri getHttpsPrefix() { - return httpsPrefix; - } - } - - WebViewLocalServer(Context context, String authority, boolean html5mode, ConfigXmlParser parser, String customScheme) { - uriMatcher = new UriMatcher(null); - this.html5mode = html5mode; - this.parser = parser; - this.protocolHandler = new AndroidProtocolHandler(context.getApplicationContext()); - this.authority = authority; - this.customScheme = customScheme; - } - - private static Uri parseAndVerifyUrl(String url) { - if (url == null) { - return null; - } - Uri uri = Uri.parse(url); - if (uri == null) { - Log.e(TAG, "Malformed URL: " + url); - return null; - } - String path = uri.getPath(); - if (path == null || path.length() == 0) { - Log.e(TAG, "URL does not have a path: " + url); - return null; - } - return uri; - } - - private static WebResourceResponse createWebResourceResponse(String mimeType, String encoding, int statusCode, String reasonPhrase, Map responseHeaders, InputStream data) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - int finalStatusCode = statusCode; - try { - if (data.available() == 0) { - finalStatusCode = 404; - } - } catch (IOException e) { - finalStatusCode = 500; - } - return new WebResourceResponse(mimeType, encoding, finalStatusCode, reasonPhrase, responseHeaders, data); - } else { - return new WebResourceResponse(mimeType, encoding, data); - } - } - - /** - * Attempt to retrieve the WebResourceResponse associated with the given request. - * This method should be invoked from within - * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, - * android.webkit.WebResourceRequest)}. - * - * @param uri the request Uri to process. - * @return a response if the request URL had a matching handler, null if no handler was found. - */ - public WebResourceResponse shouldInterceptRequest(Uri uri, WebResourceRequest request) { - PathHandler handler; - synchronized (uriMatcher) { - handler = (PathHandler) uriMatcher.match(uri); - } - if (handler == null) { - return null; - } - - if (isLocalFile(uri) || uri.getAuthority().equals(this.authority)) { - Log.d("SERVER", "Handling local request: " + uri.toString()); - return handleLocalRequest(uri, handler, request); - } else { - return handleProxyRequest(uri, handler); - } - } - - private boolean isLocalFile(Uri uri) { - String path = uri.getPath(); - if (path.startsWith(contentStart) || path.startsWith(fileStart)) { - return true; - } - return false; - } - - - private WebResourceResponse handleLocalRequest(Uri uri, PathHandler handler, WebResourceRequest request) { - String path = uri.getPath(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && request != null && request.getRequestHeaders().get("Range") != null) { - InputStream responseStream = new LollipopLazyInputStream(handler, uri); - String mimeType = getMimeType(path, responseStream); - Map tempResponseHeaders = handler.getResponseHeaders(); - int statusCode = 206; - try { - int totalRange = responseStream.available(); - String rangeString = request.getRequestHeaders().get("Range"); - String[] parts = rangeString.split("="); - String[] streamParts = parts[1].split("-"); - String fromRange = streamParts[0]; - int range = totalRange-1; - if (streamParts.length > 1) { - range = Integer.parseInt(streamParts[1]); - } - tempResponseHeaders.put("Accept-Ranges", "bytes"); - tempResponseHeaders.put("Content-Range", "bytes " + fromRange + "-" + range + "/" + totalRange); - } catch (IOException e) { - statusCode = 404; - } - return createWebResourceResponse(mimeType, handler.getEncoding(), - statusCode, handler.getReasonPhrase(), tempResponseHeaders, responseStream); - } - 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("") || 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); - } else { - stream = protocolHandler.openFile(startPath); - } - - } catch (IOException e) { - Log.e(TAG, "Unable to open " + launchFile, e); - return null; - } - - return createWebResourceResponse("text/html", handler.getEncoding(), - handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream); - } - - int periodIndex = path.lastIndexOf("."); - if (periodIndex >= 0) { - InputStream responseStream = new LollipopLazyInputStream(handler, uri); - String mimeType = getMimeType(path, responseStream); - return createWebResourceResponse(mimeType, handler.getEncoding(), - handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), responseStream); - } - - return null; - } - - /** - * Instead of reading files from the filesystem/assets, proxy through to the URL - * and let an external server handle it. - * @param uri - * @param handler - * @return - */ - private WebResourceResponse handleProxyRequest(Uri uri, PathHandler handler) { - try { - String path = uri.getPath(); - URL url = new URL(uri.toString()); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - conn.setReadTimeout(30 * 1000); - conn.setConnectTimeout(30 * 1000); - - InputStream stream = conn.getInputStream(); - - if (path.equals("/") || (!uri.getLastPathSegment().contains(".") && html5mode)) { - return createWebResourceResponse("text/html", handler.getEncoding(), - handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream); - } - - int periodIndex = path.lastIndexOf("."); - if (periodIndex >= 0) { - String ext = path.substring(path.lastIndexOf("."), path.length()); - - // TODO: Conjure up a bit more subtlety than this - if (ext.equals(".html")) { - } - - String mimeType = getMimeType(path, stream); - - return createWebResourceResponse(mimeType, handler.getEncoding(), - handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream); - } - - return createWebResourceResponse("", handler.getEncoding(), - handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream); - - } catch (SocketTimeoutException ex) { - // bridge.handleAppUrlLoadError(ex); - } catch (Exception ex) { - // bridge.handleAppUrlLoadError(ex); - } - return null; - } - - private String getMimeType(String path, InputStream stream) { - String mimeType = null; - try { - mimeType = URLConnection.guessContentTypeFromName(path); // Does not recognize *.js - if (mimeType != null && path.endsWith(".js") && mimeType.equals("image/x-icon")) { - Log.d(IonicWebViewEngine.TAG, "We shouldn't be here"); - } - if (mimeType == null) { - if (path.endsWith(".js") || path.endsWith(".mjs")) { - // Make sure JS files get the proper mimetype to support ES modules - mimeType = "application/javascript"; - } else if (path.endsWith(".wasm")) { - mimeType = "application/wasm"; - } else { - mimeType = URLConnection.guessContentTypeFromStream(stream); - } - } - } catch (Exception ex) { - Log.e(TAG, "Unable to get mime type" + path, ex); - } - return mimeType; - } - - /** - * Registers a handler for the given uri. The handler will be invoked - * every time the shouldInterceptRequest method of the instance is called with - * a matching uri. - * - * @param uri the uri to use the handler for. The scheme and authority (domain) will be matched - * exactly. The path may contain a '*' element which will match a single element of - * a path (so a handler registered for /a/* will be invoked for /a/b and /a/c.html - * but not for /a/b/b) or the '**' element which will match any number of path - * elements. - * @param handler the handler to use for the uri. - */ - void register(Uri uri, PathHandler handler) { - synchronized (uriMatcher) { - uriMatcher.addURI(uri.getScheme(), uri.getAuthority(), uri.getPath(), handler); - } - } - - /** - * Hosts the application's assets on an http(s):// URL. Assets from the local path - * assetPath/... will be available under - * http(s)://{uuid}.androidplatform.net/assets/.... - * - * @param assetPath the local path in the application's asset folder which will be made - * available by the server (for example "/www"). - */ - public void hostAssets(String assetPath) { - hostAssets(authority, assetPath); - } - - - /** - * Hosts the application's assets on an http(s):// URL. Assets from the local path - * assetPath/... will be available under - * http(s)://{domain}/{virtualAssetPath}/.... - * - * @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"). - * @return prefixes under which the assets are hosted. - */ - public void hostAssets(final String domain, - final String assetPath) { - this.isAsset = true; - this.basePath = assetPath; - - createHostingDetails(); - } - - private void createHostingDetails() { - final String assetPath = this.basePath; - - if (assetPath.indexOf('*') != -1) { - throw new IllegalArgumentException("assetPath cannot contain the '*' character."); - } - - PathHandler handler = new PathHandler() { - @Override - public InputStream handle(Uri url) { - InputStream stream = null; - String path = url.getPath(); - try { - if (path.startsWith(contentStart)) { - stream = protocolHandler.openContentUrl(url); - } else if (path.startsWith(fileStart) || !isAsset) { - if (!path.startsWith(fileStart)) { - path = basePath + url.getPath(); - } - stream = protocolHandler.openFile(path); - } else { - stream = protocolHandler.openAsset(assetPath + path); - } - } catch (IOException e) { - Log.e(TAG, "Unable to open asset URL: " + url); - return null; - } - - return stream; - } - }; - - registerUriForScheme(httpScheme, handler, authority); - registerUriForScheme(httpsScheme, handler, authority); - if (!customScheme.equals(httpScheme) && !customScheme.equals(httpsScheme)) { - registerUriForScheme(customScheme, handler, authority); - } - - } - - 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); - } - - /** - * Hosts the application's resources on an http(s):// URL. Resources - * http(s)://{uuid}.androidplatform.net/res/{resource_type}/{resource_name}. - * - * @return prefixes under which the resources are hosted. - */ - public AssetHostingDetails hostResources() { - return hostResources(authority, "/res", true, true); - } - - /** - * Hosts the application's resources on an http(s):// URL. Resources - * http(s)://{uuid}.androidplatform.net/{virtualResourcesPath}/{resource_type}/{resource_name}. - * - * @param virtualResourcesPath the path on the local server under which the resources - * 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 resources are hosted. - */ - public AssetHostingDetails hostResources(final String virtualResourcesPath, boolean enableHttp, - boolean enableHttps) { - return hostResources(authority, virtualResourcesPath, enableHttp, enableHttps); - } - - /** - * Hosts the application's resources on an http(s):// URL. Resources - * http(s)://{domain}/{virtualResourcesPath}/{resource_type}/{resource_name}. - * - * @param domain custom domain on which the assets should be hosted (for example "example.com"). - * If untrusted content is to be loaded into the WebView it is advised to make - * this random. - * @param virtualResourcesPath the path on the local server under which the resources - * 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 resources are hosted. - */ - public AssetHostingDetails hostResources(final String domain, - final String virtualResourcesPath, boolean enableHttp, - boolean enableHttps) { - if (virtualResourcesPath.indexOf('*') != -1) { - throw new IllegalArgumentException( - "virtualResourcesPath cannot contain the '*' character."); - } - - Uri.Builder uriBuilder = new Uri.Builder(); - uriBuilder.scheme(httpScheme); - uriBuilder.authority(domain); - uriBuilder.path(virtualResourcesPath); - - Uri httpPrefix = null; - Uri httpsPrefix = null; - - PathHandler handler = new PathHandler() { - @Override - public InputStream handle(Uri url) { - InputStream stream = protocolHandler.openResource(url); - 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); - } - if (enableHttps) { - uriBuilder.scheme(httpsScheme); - httpsPrefix = uriBuilder.build(); - register(Uri.withAppendedPath(httpsPrefix, "**"), handler); - } - return new AssetHostingDetails(httpPrefix, httpsPrefix); - } - - - /** - * Hosts the application's files on an http(s):// URL. Files from the basePath - * basePath/... will be available under - * http(s)://{uuid}.androidplatform.net/.... - * - * @param basePath the local path in the application's data folder which will be made - * available by the server (for example "/www"). - */ - public void hostFiles(final String basePath) { - this.isAsset = false; - this.basePath = basePath; - createHostingDetails(); - } - - /** - * The KitKat WebView reads the InputStream on a separate threadpool. We can use that to - * parallelize loading. - */ - private static abstract class LazyInputStream extends InputStream { - protected final PathHandler handler; - private InputStream is = null; - - public LazyInputStream(PathHandler handler) { - this.handler = handler; - } - - private InputStream getInputStream() { - if (is == null) { - is = handle(); - } - return is; - } - - protected abstract InputStream handle(); - - @Override - public int available() throws IOException { - InputStream is = getInputStream(); - return (is != null) ? is.available() : 0; - } - - @Override - public int read() throws IOException { - InputStream is = getInputStream(); - return (is != null) ? is.read() : -1; - } - - @Override - public int read(byte b[]) throws IOException { - InputStream is = getInputStream(); - return (is != null) ? is.read(b) : -1; - } - - @Override - public int read(byte b[], int off, int len) throws IOException { - InputStream is = getInputStream(); - return (is != null) ? is.read(b, off, len) : -1; - } - - @Override - public long skip(long n) throws IOException { - InputStream is = getInputStream(); - return (is != null) ? is.skip(n) : 0; - } - } - - // For L and above. - private static class LollipopLazyInputStream extends LazyInputStream { - private Uri uri; - private InputStream is; - - public LollipopLazyInputStream(PathHandler handler, Uri uri) { - super(handler); - this.uri = uri; - } - - @Override - protected InputStream handle() { - return handler.handle(uri); - } - } - - public String getBasePath(){ - return this.basePath; - } -} From 75ff457d2fc53325f72bca288d8c2c99e8133287 Mon Sep 17 00:00:00 2001 From: Niklas Merz Date: Sat, 28 Nov 2020 11:18:59 +0100 Subject: [PATCH 2/4] (android): add gradle dependency --- plugin.xml | 1 + .../ionicframework/cordova/webview/IonicWebViewEngine.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin.xml b/plugin.xml index e9c3bf69..acc0c7d8 100644 --- a/plugin.xml +++ b/plugin.xml @@ -50,6 +50,7 @@ + diff --git a/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java b/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java index 85a29a7d..96ecdc9f 100644 --- a/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java +++ b/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java @@ -164,11 +164,11 @@ public void onPageFinished(WebView view, String url) { public void setServerBasePath(String path) { //localServer.hostFiles(path); - //ebView.loadUrl(CDV_LOCAL_SERVER); + //webView.loadUrl(LOCAL_SERVER); } public String getServerBasePath() { //return this.localServer.getBasePath(); - return ""; + return LOCAL_SERVER; } } From 0a72fd12217192522d8744ebe6dfdd248e53477b Mon Sep 17 00:00:00 2001 From: Niklas Merz Date: Sat, 28 Nov 2020 11:35:56 +0100 Subject: [PATCH 3/4] (android): guess mimetype for JS --- .../cordova/webview/IonicWebViewEngine.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java b/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java index 96ecdc9f..e0334678 100644 --- a/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java +++ b/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java @@ -7,6 +7,7 @@ import android.graphics.Bitmap; import android.os.Build; import android.util.Log; +import android.webkit.MimeTypeMap; import android.webkit.ServiceWorkerController; import android.webkit.ServiceWorkerClient; import android.webkit.WebResourceRequest; @@ -86,7 +87,18 @@ public void init(CordovaWebView parentWebView, CordovaInterface cordova, final C if (path.isEmpty()) path = "index.html"; InputStream is = protocolHandler.openAsset("www/" + path); - @SuppressLint("RestrictedApi") String mimeType = AssetHelper.guessMimeType(path); + String mimeType = "text/html"; + String extension = MimeTypeMap.getFileExtensionFromUrl(path); + if (extension != null) { + if (path.endsWith(".js") || path.endsWith(".mjs")) { + // Make sure JS files get the proper mimetype to support ES modules + mimeType = "application/javascript"; + } else if (path.endsWith(".wasm")) { + mimeType = "application/wasm"; + } else { + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + } return new WebResourceResponse(mimeType, null, is); } catch (Exception e) { From 1b1766bfa947305be786921c8fd1b085dd1035a7 Mon Sep 17 00:00:00 2001 From: Niklas Merz Date: Sat, 28 Nov 2020 11:47:12 +0100 Subject: [PATCH 4/4] (android): remove not implemented features --- .../cordova/webview/IonicWebView.java | 26 ++----------------- .../cordova/webview/IonicWebViewEngine.java | 21 --------------- src/www/util.js | 6 ----- 3 files changed, 2 insertions(+), 51 deletions(-) diff --git a/src/android/com/ionicframework/cordova/webview/IonicWebView.java b/src/android/com/ionicframework/cordova/webview/IonicWebView.java index ff771371..a11386a8 100644 --- a/src/android/com/ionicframework/cordova/webview/IonicWebView.java +++ b/src/android/com/ionicframework/cordova/webview/IonicWebView.java @@ -1,38 +1,16 @@ package com.ionicframework.cordova.webview; -import android.app.Activity; -import android.content.SharedPreferences; - import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.json.JSONArray; -import org.json.JSONException; public class IonicWebView extends CordovaPlugin { - public static final String WEBVIEW_PREFS_NAME = "WebViewSettings"; - public static final String CDV_SERVER_PATH = "serverBasePath"; - - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) { - if (action.equals("setServerBasePath")) { - final String path = args.getString(0); - cordova.getActivity().runOnUiThread(new Runnable() { - public void run() { - ((IonicWebViewEngine)webView.getEngine()).setServerBasePath(path); - } - }); - return true; - } else if (action.equals("getServerBasePath")) { + if (action.equals("getServerBasePath")) { callbackContext.success(((IonicWebViewEngine)webView.getEngine()).getServerBasePath()); return true; - } else if (action.equals("persistServerBasePath")) { - String path = ((IonicWebViewEngine)webView.getEngine()).getServerBasePath(); - SharedPreferences prefs = cordova.getActivity().getApplicationContext().getSharedPreferences(WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(CDV_SERVER_PATH, path); - editor.apply(); - return true; } return false; } diff --git a/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java b/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java index e0334678..b63a6644 100644 --- a/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java +++ b/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java @@ -1,9 +1,6 @@ package com.ionicframework.cordova.webview; -import android.annotation.SuppressLint; -import android.app.Activity; import android.content.Context; -import android.content.SharedPreferences; import android.graphics.Bitmap; import android.os.Build; import android.util.Log; @@ -28,7 +25,6 @@ import org.apache.cordova.engine.SystemWebView; import androidx.webkit.WebViewAssetLoader; -import androidx.webkit.internal.AssetHelper; import java.io.InputStream; @@ -77,11 +73,6 @@ public void init(CordovaWebView parentWebView, CordovaInterface cordova, final C assetLoader = new WebViewAssetLoader.Builder() .setDomain(hostname) .setHttpAllowed(true) - // ---- Path handler - // Default path handler not working - // .addPathHandler("/assets/", new WebViewAssetLoader.AssetsPathHandler(this)) - // .addPathHandler("/res/", new WebViewAssetLoader.ResourcesPathHandler(this)) - // => implementing custom handler .addPathHandler("/", path -> { try { if (path.isEmpty()) @@ -118,9 +109,6 @@ public void init(CordovaWebView parentWebView, CordovaInterface cordova, final C int mode = preferences.getInteger("MixedContentMode", 0); settings.setMixedContentMode(mode); } - SharedPreferences prefs = cordova.getActivity().getApplicationContext().getSharedPreferences(IonicWebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE); - String path = prefs.getString(IonicWebView.CDV_SERVER_PATH, null); - boolean setAsServiceWorkerClient = preferences.getBoolean("ResolveServiceWorkerRequests", false); ServiceWorkerController controller = null; @@ -168,19 +156,10 @@ public void onPageStarted(WebView view, String url, Bitmap favicon) { @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); - view.loadUrl("javascript:(function() { " + - "window.WEBVIEW_SERVER_URL = '" + LOCAL_SERVER + "';" + - "})()"); } } - public void setServerBasePath(String path) { - //localServer.hostFiles(path); - //webView.loadUrl(LOCAL_SERVER); - } - public String getServerBasePath() { - //return this.localServer.getBasePath(); return LOCAL_SERVER; } } diff --git a/src/www/util.js b/src/www/util.js index ba52a8e9..c5f834fc 100644 --- a/src/www/util.js +++ b/src/www/util.js @@ -16,14 +16,8 @@ var WebView = { } return url; }, - setServerBasePath: function(path) { - exec(null, null, 'IonicWebView', 'setServerBasePath', [path]); - }, getServerBasePath: function(callback) { exec(callback, null, 'IonicWebView', 'getServerBasePath', []); - }, - persistServerBasePath: function() { - exec(null, null, 'IonicWebView', 'persistServerBasePath', []); } }