Initial Commit on git.saherelm.ir ...
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
package com.ionicframework.cordova.webview;
|
||||
|
||||
// Copyright 2012 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class AndroidProtocolHandler {
|
||||
private static final String TAG = "AndroidProtocolHandler";
|
||||
|
||||
private Context context;
|
||||
|
||||
public AndroidProtocolHandler(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public InputStream openAsset(String path) throws IOException {
|
||||
return context.getAssets().open(path, AssetManager.ACCESS_STREAMING);
|
||||
}
|
||||
|
||||
public InputStream openResource(Uri uri) {
|
||||
assert uri.getPath() != null;
|
||||
// The path must be of the form ".../asset_type/asset_name.ext".
|
||||
List<String> pathSegments = uri.getPathSegments();
|
||||
String assetType = pathSegments.get(pathSegments.size() - 2);
|
||||
String assetName = pathSegments.get(pathSegments.size() - 1);
|
||||
|
||||
// Drop the file extension.
|
||||
assetName = assetName.split("\\.")[0];
|
||||
try {
|
||||
// Use the application context for resolving the resource package name so that we do
|
||||
// not use the browser's own resources. Note that if 'context' here belongs to the
|
||||
// test suite, it does not have a separate application context. In that case we use
|
||||
// the original context object directly.
|
||||
if (context.getApplicationContext() != null) {
|
||||
context = context.getApplicationContext();
|
||||
}
|
||||
int fieldId = getFieldId(context, assetType, assetName);
|
||||
int valueType = getValueType(context, fieldId);
|
||||
if (valueType == TypedValue.TYPE_STRING) {
|
||||
return context.getResources().openRawResource(fieldId);
|
||||
} else {
|
||||
Log.e(TAG, "Asset not of type string: " + uri);
|
||||
return null;
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.e(TAG, "Unable to open resource URL: " + uri, e);
|
||||
return null;
|
||||
} catch (NoSuchFieldException e) {
|
||||
Log.e(TAG, "Unable to open resource URL: " + uri, e);
|
||||
return null;
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.e(TAG, "Unable to open resource URL: " + uri, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream openFile(String filePath) throws IOException {
|
||||
String realPath = filePath.replace(WebViewLocalServer.fileStart, "");
|
||||
File localFile = new File(realPath);
|
||||
return new FileInputStream(localFile);
|
||||
}
|
||||
|
||||
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:/");
|
||||
} else {
|
||||
realPath = uri.toString().replace(uri.getScheme() + "://" + uri.getHost() + ":" + port + WebViewLocalServer.contentStart, "content:/");
|
||||
}
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = context.getContentResolver().openInputStream(Uri.parse(realPath));
|
||||
} 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()
|
||||
.loadClass(context.getPackageName() + ".R$" + assetType);
|
||||
java.lang.reflect.Field field = d.getField(assetName);
|
||||
int id = field.getInt(null);
|
||||
return id;
|
||||
}
|
||||
|
||||
private static int getValueType(Context context, int fieldId) {
|
||||
TypedValue value = new TypedValue();
|
||||
context.getResources().getValue(fieldId, value, true);
|
||||
return value.type;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
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 {
|
||||
|
||||
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")) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package com.ionicframework.cordova.webview;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.util.Log;
|
||||
import android.webkit.ServiceWorkerController;
|
||||
import android.webkit.ServiceWorkerClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
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;
|
||||
import org.apache.cordova.CordovaResourceApi;
|
||||
import org.apache.cordova.CordovaWebView;
|
||||
import org.apache.cordova.CordovaWebViewEngine;
|
||||
import org.apache.cordova.NativeToJsMessageQueue;
|
||||
import org.apache.cordova.PluginManager;
|
||||
import org.apache.cordova.engine.SystemWebViewClient;
|
||||
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);
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
return localServer.shouldInterceptRequest(request.getUrl(), request);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
|
||||
return localServer.shouldInterceptRequest(Uri.parse(url), null);
|
||||
}
|
||||
|
||||
@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 += "/";
|
||||
}
|
||||
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 + "';" +
|
||||
"})()");
|
||||
}
|
||||
}
|
||||
|
||||
public void setServerBasePath(String path) {
|
||||
localServer.hostFiles(path);
|
||||
webView.loadUrl(CDV_LOCAL_SERVER);
|
||||
}
|
||||
|
||||
public String getServerBasePath() {
|
||||
return this.localServer.getBasePath();
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
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<UriMatcher>();
|
||||
mText = null;
|
||||
}
|
||||
|
||||
private UriMatcher() {
|
||||
mCode = null;
|
||||
mWhich = -1;
|
||||
mChildren = new ArrayList<UriMatcher>();
|
||||
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.
|
||||
* <p>
|
||||
* 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<UriMatcher> 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<String> 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<UriMatcher> 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<UriMatcher> mChildren;
|
||||
}
|
||||
+645
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Methods of this handler will be invoked on a background thread and care must be taken to
|
||||
* correctly synchronize access to any shared state.
|
||||
* <p>
|
||||
* 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<String, String> responseHeaders;
|
||||
|
||||
public PathHandler() {
|
||||
this(null, null, 200, "OK", null);
|
||||
}
|
||||
|
||||
public PathHandler(String encoding, String charset, int statusCode,
|
||||
String reasonPhrase, Map<String, String> responseHeaders) {
|
||||
this.encoding = encoding;
|
||||
this.charset = charset;
|
||||
this.statusCode = statusCode;
|
||||
this.reasonPhrase = reasonPhrase;
|
||||
Map<String, String> tempResponseHeaders;
|
||||
if (responseHeaders == null) {
|
||||
tempResponseHeaders = new HashMap<String, String>();
|
||||
} 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<String, String> 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<String, String> 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 <code>request</code>.
|
||||
* 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<String, String> 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 <code>uri</code>. The <code>handler</code> will be invoked
|
||||
* every time the <code>shouldInterceptRequest</code> method of the instance is called with
|
||||
* a matching <code>uri</code>.
|
||||
*
|
||||
* @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
|
||||
* <code>assetPath/...</code> will be available under
|
||||
* <code>http(s)://{uuid}.androidplatform.net/assets/...</code>.
|
||||
*
|
||||
* @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
|
||||
* <code>assetPath/...</code> will be available under
|
||||
* <code>http(s)://{domain}/{virtualAssetPath}/...</code>.
|
||||
*
|
||||
* @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
|
||||
* <code>http(s)://{uuid}.androidplatform.net/res/{resource_type}/{resource_name}</code>.
|
||||
*
|
||||
* @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
|
||||
* <code>http(s)://{uuid}.androidplatform.net/{virtualResourcesPath}/{resource_type}/{resource_name}</code>.
|
||||
*
|
||||
* @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
|
||||
* <code>http(s)://{domain}/{virtualResourcesPath}/{resource_type}/{resource_name}</code>.
|
||||
*
|
||||
* @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
|
||||
* <code>basePath/...</code> will be available under
|
||||
* <code>http(s)://{uuid}.androidplatform.net/...</code>.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you 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.
|
||||
*/
|
||||
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
@interface CDVWKProcessPoolFactory : NSObject
|
||||
@property (nonatomic, retain) WKProcessPool* sharedPool;
|
||||
|
||||
+(instancetype) sharedFactory;
|
||||
-(WKProcessPool*) sharedProcessPool;
|
||||
@end
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you 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.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
#import "CDVWKProcessPoolFactory.h"
|
||||
|
||||
static CDVWKProcessPoolFactory *factory = nil;
|
||||
|
||||
@implementation CDVWKProcessPoolFactory
|
||||
|
||||
+ (instancetype)sharedFactory
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
factory = [[CDVWKProcessPoolFactory alloc] init];
|
||||
});
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_sharedPool = [[WKProcessPool alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (WKProcessPool*) sharedProcessPool {
|
||||
return _sharedPool;
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you 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.
|
||||
*/
|
||||
|
||||
#import <WebKit/WebKit.h>
|
||||
#import <Cordova/CDV.h>
|
||||
|
||||
@interface CDVWKWebViewEngine : CDVPlugin <CDVWebViewEngineProtocol, WKScriptMessageHandler, WKNavigationDelegate>
|
||||
|
||||
@property (nonatomic, strong, readonly) id <WKUIDelegate> uiDelegate;
|
||||
@property (nonatomic, strong) NSString * basePath;
|
||||
|
||||
-(void)setServerBasePath:(CDVInvokedUrlCommand*)command;
|
||||
-(void)getServerBasePath:(CDVInvokedUrlCommand*)command;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,825 @@
|
||||
/*
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you 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.
|
||||
*/
|
||||
|
||||
#import <Cordova/NSDictionary+CordovaPreferences.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <objc/message.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "CDVWKWebViewEngine.h"
|
||||
#import "CDVWKWebViewUIDelegate.h"
|
||||
#import "CDVWKProcessPoolFactory.h"
|
||||
#import "IONAssetHandler.h"
|
||||
|
||||
#define CDV_BRIDGE_NAME @"cordova"
|
||||
#define CDV_IONIC_STOP_SCROLL @"stopScroll"
|
||||
#define CDV_SERVER_PATH @"serverBasePath"
|
||||
#define LAST_BINARY_VERSION_CODE @"lastBinaryVersionCode"
|
||||
#define LAST_BINARY_VERSION_NAME @"lastBinaryVersionName"
|
||||
|
||||
@implementation UIScrollView (BugIOS11)
|
||||
|
||||
+ (void)load {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
Class class = [self class];
|
||||
SEL originalSelector = @selector(init);
|
||||
SEL swizzledSelector = @selector(xxx_init);
|
||||
|
||||
Method originalMethod = class_getInstanceMethod(class, originalSelector);
|
||||
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
|
||||
|
||||
BOOL didAddMethod =
|
||||
class_addMethod(class,
|
||||
originalSelector,
|
||||
method_getImplementation(swizzledMethod),
|
||||
method_getTypeEncoding(swizzledMethod));
|
||||
|
||||
if (didAddMethod) {
|
||||
class_replaceMethod(class,
|
||||
swizzledSelector,
|
||||
method_getImplementation(originalMethod),
|
||||
method_getTypeEncoding(originalMethod));
|
||||
} else {
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Method Swizzling
|
||||
|
||||
- (id)xxx_init {
|
||||
id a = [self xxx_init];
|
||||
NSArray *stack = [NSThread callStackSymbols];
|
||||
for(NSString *trace in stack) {
|
||||
if([trace containsString:@"WebKit"]) {
|
||||
[a setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface CDVWKWeakScriptMessageHandler : NSObject <WKScriptMessageHandler>
|
||||
|
||||
@property (nonatomic, weak, readonly) id<WKScriptMessageHandler>scriptMessageHandler;
|
||||
|
||||
- (instancetype)initWithScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface CDVWKWebViewEngine ()
|
||||
|
||||
@property (nonatomic, strong, readwrite) UIView* engineWebView;
|
||||
@property (nonatomic, strong, readwrite) id <WKUIDelegate> uiDelegate;
|
||||
@property (nonatomic, weak) id <WKScriptMessageHandler> weakScriptMessageHandler;
|
||||
@property (nonatomic, readwrite) CGRect frame;
|
||||
@property (nonatomic, strong) NSString *userAgentCreds;
|
||||
@property (nonatomic, strong) IONAssetHandler * handler;
|
||||
|
||||
@property (nonatomic, readwrite) NSString *CDV_LOCAL_SERVER;
|
||||
@end
|
||||
|
||||
// expose private configuration value required for background operation
|
||||
@interface WKWebViewConfiguration ()
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// see forwardingTargetForSelector: selector comment for the reason for this pragma
|
||||
#pragma clang diagnostic ignored "-Wprotocol"
|
||||
|
||||
@implementation CDVWKWebViewEngine
|
||||
|
||||
@synthesize engineWebView = _engineWebView;
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
if (NSClassFromString(@"WKWebView") == nil) {
|
||||
return nil;
|
||||
}
|
||||
// add to keyWindow to ensure it is 'active'
|
||||
[UIApplication.sharedApplication.keyWindow addSubview:self.engineWebView];
|
||||
|
||||
self.frame = frame;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
-(NSString *) getStartPath {
|
||||
NSString * wwwPath = [[NSBundle mainBundle] pathForResource:@"www" ofType: nil];
|
||||
|
||||
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
|
||||
NSString * persistedPath = [userDefaults objectForKey:CDV_SERVER_PATH];
|
||||
if (![self isDeployDisabled] && ![self isNewBinary] && persistedPath && ![persistedPath isEqualToString:@""]) {
|
||||
NSString *libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
|
||||
NSString * cordovaDataDirectory = [libPath stringByAppendingPathComponent:@"NoCloud"];
|
||||
NSString * snapshots = [cordovaDataDirectory stringByAppendingPathComponent:@"ionic_built_snapshots"];
|
||||
wwwPath = [snapshots stringByAppendingPathComponent:[persistedPath lastPathComponent]];
|
||||
}
|
||||
self.basePath = wwwPath;
|
||||
return wwwPath;
|
||||
}
|
||||
|
||||
-(BOOL) isNewBinary
|
||||
{
|
||||
NSString * versionCode = [[NSBundle mainBundle] infoDictionary][@"CFBundleVersion"];
|
||||
NSString * versionName = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
|
||||
NSUserDefaults * prefs = [NSUserDefaults standardUserDefaults];
|
||||
NSString * lastVersionCode = [prefs stringForKey:LAST_BINARY_VERSION_CODE];
|
||||
NSString * lastVersionName = [prefs stringForKey:LAST_BINARY_VERSION_NAME];
|
||||
if (![versionCode isEqualToString:lastVersionCode] || ![versionName isEqualToString:lastVersionName]) {
|
||||
[prefs setObject:versionCode forKey:LAST_BINARY_VERSION_CODE];
|
||||
[prefs setObject:versionName forKey:LAST_BINARY_VERSION_NAME];
|
||||
[prefs setObject:@"" forKey:CDV_SERVER_PATH];
|
||||
[prefs synchronize];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
-(BOOL) isDeployDisabled {
|
||||
return [[self.commandDelegate.settings objectForKey:[@"DisableDeploy" lowercaseString]] boolValue];
|
||||
}
|
||||
|
||||
- (WKWebViewConfiguration*) createConfigurationFromSettings:(NSDictionary*)settings
|
||||
{
|
||||
WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
|
||||
configuration.processPool = [[CDVWKProcessPoolFactory sharedFactory] sharedProcessPool];
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
|
||||
|
||||
if (settings == nil) {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
if(![settings cordovaBoolSettingForKey:@"WKSuspendInBackground" defaultValue:YES]){
|
||||
NSString* _BGStatus;
|
||||
if (@available(iOS 12.2, *)) {
|
||||
// do stuff for iOS 12.2 and newer
|
||||
NSLog(@"iOS 12.2+ detected");
|
||||
NSString* str = @"YWx3YXlzUnVuc0F0Rm9yZWdyb3VuZFByaW9yaXR5";
|
||||
NSData* data = [[NSData alloc] initWithBase64EncodedString:str options:0];
|
||||
_BGStatus = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
} else {
|
||||
// do stuff for iOS 12.1 and older
|
||||
NSLog(@"iOS Below 12.2 detected");
|
||||
NSString* str = @"X2Fsd2F5c1J1bnNBdEZvcmVncm91bmRQcmlvcml0eQ==";
|
||||
NSData* data = [[NSData alloc] initWithBase64EncodedString:str options:0];
|
||||
_BGStatus = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
[configuration setValue:[NSNumber numberWithBool:YES]
|
||||
forKey:_BGStatus];
|
||||
}
|
||||
NSString *userAgent = configuration.applicationNameForUserAgent;
|
||||
if (
|
||||
[settings cordovaSettingForKey:@"OverrideUserAgent"] == nil &&
|
||||
[settings cordovaSettingForKey:@"AppendUserAgent"] != nil
|
||||
) {
|
||||
userAgent = [NSString stringWithFormat:@"%@ %@", userAgent, [settings cordovaSettingForKey:@"AppendUserAgent"]];
|
||||
}
|
||||
configuration.applicationNameForUserAgent = userAgent;
|
||||
configuration.allowsInlineMediaPlayback = [settings cordovaBoolSettingForKey:@"AllowInlineMediaPlayback" defaultValue:YES];
|
||||
configuration.suppressesIncrementalRendering = [settings cordovaBoolSettingForKey:@"SuppressesIncrementalRendering" defaultValue:NO];
|
||||
configuration.allowsAirPlayForMediaPlayback = [settings cordovaBoolSettingForKey:@"MediaPlaybackAllowsAirPlay" defaultValue:YES];
|
||||
return configuration;
|
||||
}
|
||||
|
||||
- (void)pluginInitialize
|
||||
{
|
||||
// viewController would be available now. we attempt to set all possible delegates to it, by default
|
||||
NSDictionary* settings = self.commandDelegate.settings;
|
||||
NSString *bind = [settings cordovaSettingForKey:@"Hostname"];
|
||||
if(bind == nil){
|
||||
bind = @"localhost";
|
||||
}
|
||||
NSString *scheme = [settings cordovaSettingForKey:@"iosScheme"];
|
||||
if(scheme == nil || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"] || [scheme isEqualToString:@"file"]){
|
||||
scheme = @"ionic";
|
||||
}
|
||||
self.CDV_LOCAL_SERVER = [NSString stringWithFormat:@"%@://%@", scheme, bind];
|
||||
|
||||
self.uiDelegate = [[CDVWKWebViewUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]];
|
||||
|
||||
CDVWKWeakScriptMessageHandler *weakScriptMessageHandler = [[CDVWKWeakScriptMessageHandler alloc] initWithScriptMessageHandler:self];
|
||||
|
||||
WKUserContentController* userContentController = [[WKUserContentController alloc] init];
|
||||
[userContentController addScriptMessageHandler:weakScriptMessageHandler name:CDV_BRIDGE_NAME];
|
||||
[userContentController addScriptMessageHandler:weakScriptMessageHandler name:CDV_IONIC_STOP_SCROLL];
|
||||
|
||||
// Inject XHR Polyfill
|
||||
NSLog(@"CDVWKWebViewEngine: trying to inject XHR polyfill");
|
||||
WKUserScript *wkScript = [self wkPluginScript];
|
||||
if (wkScript) {
|
||||
[userContentController addUserScript:wkScript];
|
||||
}
|
||||
|
||||
WKUserScript *configScript = [self configScript];
|
||||
if (configScript) {
|
||||
[userContentController addUserScript:configScript];
|
||||
}
|
||||
|
||||
BOOL autoCordova = [settings cordovaBoolSettingForKey:@"AutoInjectCordova" defaultValue:NO];
|
||||
if (autoCordova){
|
||||
NSLog(@"CDVWKWebViewEngine: trying to inject XHR polyfill");
|
||||
WKUserScript *cordova = [self autoCordovify];
|
||||
if (cordova) {
|
||||
[userContentController addUserScript:cordova];
|
||||
}
|
||||
}
|
||||
|
||||
BOOL audioCanMix = [settings cordovaBoolSettingForKey:@"AudioCanMix" defaultValue:NO];
|
||||
if (audioCanMix) {
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
|
||||
withOptions:AVAudioSessionCategoryOptionMixWithOthers
|
||||
error:nil];
|
||||
}
|
||||
|
||||
WKWebViewConfiguration* configuration = [self createConfigurationFromSettings:settings];
|
||||
configuration.userContentController = userContentController;
|
||||
|
||||
self.handler = [[IONAssetHandler alloc] initWithBasePath:[self getStartPath] andScheme:scheme];
|
||||
[configuration setURLSchemeHandler:self.handler forURLScheme:scheme];
|
||||
|
||||
// re-create WKWebView, since we need to update configuration
|
||||
// remove from keyWindow before recreating
|
||||
[self.engineWebView removeFromSuperview];
|
||||
WKWebView* wkWebView = [[WKWebView alloc] initWithFrame:self.frame configuration:configuration];
|
||||
|
||||
[wkWebView.scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
|
||||
|
||||
wkWebView.UIDelegate = self.uiDelegate;
|
||||
self.engineWebView = wkWebView;
|
||||
// add to keyWindow to ensure it is 'active'
|
||||
[UIApplication.sharedApplication.keyWindow addSubview:self.engineWebView];
|
||||
|
||||
NSString * overrideUserAgent = [settings cordovaSettingForKey:@"OverrideUserAgent"];
|
||||
if (overrideUserAgent != nil) {
|
||||
wkWebView.customUserAgent = overrideUserAgent;
|
||||
}
|
||||
|
||||
if ([self.viewController conformsToProtocol:@protocol(WKUIDelegate)]) {
|
||||
wkWebView.UIDelegate = (id <WKUIDelegate>)self.viewController;
|
||||
}
|
||||
|
||||
if ([self.viewController conformsToProtocol:@protocol(WKNavigationDelegate)]) {
|
||||
wkWebView.navigationDelegate = (id <WKNavigationDelegate>)self.viewController;
|
||||
} else {
|
||||
wkWebView.navigationDelegate = (id <WKNavigationDelegate>)self;
|
||||
}
|
||||
|
||||
if ([self.viewController conformsToProtocol:@protocol(WKScriptMessageHandler)]) {
|
||||
[wkWebView.configuration.userContentController addScriptMessageHandler:(id < WKScriptMessageHandler >)self.viewController name:CDV_BRIDGE_NAME];
|
||||
}
|
||||
|
||||
[self keyboardDisplayDoesNotRequireUserAction];
|
||||
|
||||
if ([settings cordovaBoolSettingForKey:@"KeyboardAppearanceDark" defaultValue:NO]) {
|
||||
[self setKeyboardAppearanceDark];
|
||||
}
|
||||
|
||||
[self updateSettings:settings];
|
||||
|
||||
// check if content thread has died on resume
|
||||
NSLog(@"%@", @"CDVWKWebViewEngine will reload WKWebView if required on resume");
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver:self
|
||||
selector:@selector(onAppWillEnterForeground:)
|
||||
name:UIApplicationWillEnterForegroundNotification object:nil];
|
||||
|
||||
// If less than ios 13.4
|
||||
if (@available(iOS 13.4, *)) {} else {
|
||||
// For keyboard dismissal leaving viewport shifted (can potentially be removed when apple releases the fix for the issue discussed here: https://github.com/apache/cordova-ios/issues/417#issuecomment-423340885)
|
||||
// Apple has released a fix in 13.4, but not in 12.x (as of 12.4.6)
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver:self
|
||||
selector:@selector(keyboardWillHide)
|
||||
name:UIKeyboardWillHideNotification object:nil];
|
||||
}
|
||||
|
||||
NSLog(@"Using Ionic WKWebView");
|
||||
|
||||
}
|
||||
|
||||
// https://github.com/Telerik-Verified-Plugins/WKWebView/commit/04e8296adeb61f289f9c698045c19b62d080c7e3#L609-L620
|
||||
- (void) keyboardDisplayDoesNotRequireUserAction {
|
||||
Class class = NSClassFromString(@"WKContentView");
|
||||
NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0};
|
||||
NSOperatingSystemVersion iOS_12_2_0 = (NSOperatingSystemVersion){12, 2, 0};
|
||||
NSOperatingSystemVersion iOS_13_0_0 = (NSOperatingSystemVersion){13, 0, 0};
|
||||
char * methodSignature = "_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:";
|
||||
|
||||
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_13_0_0]) {
|
||||
methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:";
|
||||
} else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_12_2_0]) {
|
||||
methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:";
|
||||
}
|
||||
|
||||
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) {
|
||||
SEL selector = sel_getUid(methodSignature);
|
||||
Method method = class_getInstanceMethod(class, selector);
|
||||
IMP original = method_getImplementation(method);
|
||||
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
|
||||
((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
|
||||
});
|
||||
method_setImplementation(method, override);
|
||||
} else {
|
||||
SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:");
|
||||
Method method = class_getInstanceMethod(class, selector);
|
||||
IMP original = method_getImplementation(method);
|
||||
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) {
|
||||
((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3);
|
||||
});
|
||||
method_setImplementation(method, override);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setKeyboardAppearanceDark
|
||||
{
|
||||
IMP darkImp = imp_implementationWithBlock(^(id _s) {
|
||||
return UIKeyboardAppearanceDark;
|
||||
});
|
||||
for (NSString* classString in @[@"WKContentView", @"UITextInputTraits"]) {
|
||||
Class c = NSClassFromString(classString);
|
||||
Method m = class_getInstanceMethod(c, @selector(keyboardAppearance));
|
||||
if (m != NULL) {
|
||||
method_setImplementation(m, darkImp);
|
||||
} else {
|
||||
class_addMethod(c, @selector(keyboardAppearance), darkImp, "l@:");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (void)onAppWillEnterForeground:(NSNotification *)notification {
|
||||
if ([self shouldReloadWebView]) {
|
||||
NSLog(@"%@", @"CDVWKWebViewEngine reloading!");
|
||||
[(WKWebView*)_engineWebView reload];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-(void)keyboardWillHide
|
||||
{
|
||||
// For keyboard dismissal leaving viewport shifted (can potentially be removed when apple releases the fix for the issue discussed here: https://github.com/apache/cordova-ios/issues/417#issuecomment-423340885)
|
||||
UIScrollView * scrollView = self.webView.scrollView;
|
||||
// Calculate some vars for convenience
|
||||
CGFloat contentLengthWithInsets = scrollView.contentSize.height + scrollView.adjustedContentInset.top + scrollView.adjustedContentInset.bottom;
|
||||
CGFloat contentOffsetY = scrollView.contentOffset.y;
|
||||
CGFloat screenHeight = scrollView.frame.size.height;
|
||||
CGFloat maxAllowedOffsetY = fmax(contentLengthWithInsets - screenHeight, 0); // 0 is for the case where content is shorter than screen
|
||||
|
||||
// If the keyboard allowed the user to get to an offset beyond the max
|
||||
if (contentOffsetY > maxAllowedOffsetY) {
|
||||
// Reset the scroll to the max allowed so that there is no additional empty white space at the bottom where the keyboard occupied!
|
||||
CGPoint bottomOfPage = CGPointMake(scrollView.contentOffset.x, maxAllowedOffsetY);
|
||||
[scrollView setContentOffset:bottomOfPage];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)shouldReloadWebView
|
||||
{
|
||||
WKWebView* wkWebView = (WKWebView*)_engineWebView;
|
||||
return [self shouldReloadWebView:wkWebView.URL title:wkWebView.title];
|
||||
}
|
||||
|
||||
- (BOOL)shouldReloadWebView:(NSURL *)location title:(NSString*)title
|
||||
{
|
||||
BOOL title_is_nil = (title == nil);
|
||||
BOOL location_is_blank = [[location absoluteString] isEqualToString:@"about:blank"];
|
||||
|
||||
BOOL reload = (title_is_nil || location_is_blank);
|
||||
|
||||
#ifdef DEBUG
|
||||
NSLog(@"%@", @"CDVWKWebViewEngine shouldReloadWebView::");
|
||||
NSLog(@"CDVWKWebViewEngine shouldReloadWebView title: %@", title);
|
||||
NSLog(@"CDVWKWebViewEngine shouldReloadWebView location: %@", [location absoluteString]);
|
||||
NSLog(@"CDVWKWebViewEngine shouldReloadWebView reload: %u", reload);
|
||||
#endif
|
||||
|
||||
return reload;
|
||||
}
|
||||
|
||||
|
||||
- (id)loadRequest:(NSURLRequest *)request
|
||||
{
|
||||
if (request.URL.fileURL) {
|
||||
NSURL* startURL = [NSURL URLWithString:((CDVViewController *)self.viewController).startPage];
|
||||
NSString* startFilePath = [self.commandDelegate pathForResource:[startURL path]];
|
||||
NSURL *url = [[NSURL URLWithString:self.CDV_LOCAL_SERVER] URLByAppendingPathComponent:request.URL.path];
|
||||
if ([request.URL.path isEqualToString:startFilePath]) {
|
||||
url = [NSURL URLWithString:self.CDV_LOCAL_SERVER];
|
||||
}
|
||||
if(request.URL.query) {
|
||||
url = [NSURL URLWithString:[@"?" stringByAppendingString:request.URL.query] relativeToURL:url];
|
||||
}
|
||||
if(request.URL.fragment) {
|
||||
url = [NSURL URLWithString:[@"#" stringByAppendingString:request.URL.fragment] relativeToURL:url];
|
||||
}
|
||||
request = [NSURLRequest requestWithURL:url];
|
||||
}
|
||||
return [(WKWebView*)_engineWebView loadRequest:request];
|
||||
}
|
||||
|
||||
- (id)loadHTMLString:(NSString *)string baseURL:(NSURL*)baseURL
|
||||
{
|
||||
return [(WKWebView*)_engineWebView loadHTMLString:string baseURL:baseURL];
|
||||
}
|
||||
|
||||
- (NSURL*) URL
|
||||
{
|
||||
return [(WKWebView*)_engineWebView URL];
|
||||
}
|
||||
|
||||
- (BOOL)canLoadRequest:(NSURLRequest *)request
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
- (void)updateSettings:(NSDictionary *)settings
|
||||
{
|
||||
WKWebView* wkWebView = (WKWebView *)_engineWebView;
|
||||
|
||||
// By default, DisallowOverscroll is false (thus bounce is allowed)
|
||||
BOOL bounceAllowed = !([settings cordovaBoolSettingForKey:@"DisallowOverscroll" defaultValue:NO]);
|
||||
|
||||
// prevent webView from bouncing
|
||||
if (!bounceAllowed) {
|
||||
if ([wkWebView respondsToSelector:@selector(scrollView)]) {
|
||||
((UIScrollView*)[wkWebView scrollView]).bounces = NO;
|
||||
} else {
|
||||
for (id subview in wkWebView.subviews) {
|
||||
if ([[subview class] isSubclassOfClass:[UIScrollView class]]) {
|
||||
((UIScrollView*)subview).bounces = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wkWebView.configuration.preferences.minimumFontSize = [settings cordovaFloatSettingForKey:@"MinimumFontSize" defaultValue:0.0];
|
||||
wkWebView.allowsLinkPreview = [settings cordovaBoolSettingForKey:@"AllowLinkPreview" defaultValue:NO];
|
||||
wkWebView.scrollView.scrollEnabled = [settings cordovaBoolSettingForKey:@"ScrollEnabled" defaultValue:NO];
|
||||
wkWebView.allowsBackForwardNavigationGestures = [settings cordovaBoolSettingForKey:@"AllowBackForwardNavigationGestures" defaultValue:NO];
|
||||
}
|
||||
|
||||
- (void)updateWithInfo:(NSDictionary *)info
|
||||
{
|
||||
NSDictionary* scriptMessageHandlers = [info objectForKey:kCDVWebViewEngineScriptMessageHandlers];
|
||||
NSDictionary* settings = [info objectForKey:kCDVWebViewEngineWebViewPreferences];
|
||||
id navigationDelegate = [info objectForKey:kCDVWebViewEngineWKNavigationDelegate];
|
||||
id uiDelegate = [info objectForKey:kCDVWebViewEngineWKUIDelegate];
|
||||
|
||||
WKWebView* wkWebView = (WKWebView*)_engineWebView;
|
||||
|
||||
if (scriptMessageHandlers && [scriptMessageHandlers isKindOfClass:[NSDictionary class]]) {
|
||||
NSArray* allKeys = [scriptMessageHandlers allKeys];
|
||||
|
||||
for (NSString* key in allKeys) {
|
||||
id object = [scriptMessageHandlers objectForKey:key];
|
||||
if ([object conformsToProtocol:@protocol(WKScriptMessageHandler)]) {
|
||||
[wkWebView.configuration.userContentController addScriptMessageHandler:object name:key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (navigationDelegate && [navigationDelegate conformsToProtocol:@protocol(WKNavigationDelegate)]) {
|
||||
wkWebView.navigationDelegate = navigationDelegate;
|
||||
}
|
||||
|
||||
if (uiDelegate && [uiDelegate conformsToProtocol:@protocol(WKUIDelegate)]) {
|
||||
wkWebView.UIDelegate = uiDelegate;
|
||||
}
|
||||
|
||||
if (settings && [settings isKindOfClass:[NSDictionary class]]) {
|
||||
[self updateSettings:settings];
|
||||
}
|
||||
}
|
||||
|
||||
// This forwards the methods that are in the header that are not implemented here.
|
||||
// loadHTMLString:baseURL:
|
||||
// loadRequest:
|
||||
- (id)forwardingTargetForSelector:(SEL)aSelector
|
||||
{
|
||||
return _engineWebView;
|
||||
}
|
||||
|
||||
- (UIView *)webView
|
||||
{
|
||||
return self.engineWebView;
|
||||
}
|
||||
|
||||
- (WKUserScript *)wkPluginScript
|
||||
{
|
||||
NSString *scriptFile = [[NSBundle mainBundle] pathForResource:@"www/wk-plugin" ofType:@"js"];
|
||||
if (scriptFile == nil) {
|
||||
NSLog(@"CDVWKWebViewEngine: WK plugin was not found");
|
||||
return nil;
|
||||
}
|
||||
NSError *error = nil;
|
||||
NSString *source = [NSString stringWithContentsOfFile:scriptFile encoding:NSUTF8StringEncoding error:&error];
|
||||
if (source == nil || error != nil) {
|
||||
NSLog(@"CDVWKWebViewEngine: WK plugin can not be loaded: %@", error);
|
||||
return nil;
|
||||
}
|
||||
source = [source stringByAppendingString:[NSString stringWithFormat:@"window.WEBVIEW_SERVER_URL = '%@';", self.CDV_LOCAL_SERVER]];
|
||||
|
||||
return [[WKUserScript alloc] initWithSource:source
|
||||
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
|
||||
forMainFrameOnly:YES];
|
||||
}
|
||||
|
||||
- (WKUserScript *)configScript
|
||||
{
|
||||
Class keyboard = NSClassFromString(@"CDVIonicKeyboard");
|
||||
BOOL keyboardPlugin = keyboard != nil;
|
||||
if(!keyboardPlugin) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL keyboardResizes = [self.commandDelegate.settings cordovaBoolSettingForKey:@"KeyboardResize" defaultValue:YES];
|
||||
NSString *source = [NSString stringWithFormat:
|
||||
@"window.Ionic = window.Ionic || {};"
|
||||
@"window.Ionic.keyboardPlugin=true;"
|
||||
@"window.Ionic.keyboardResizes=%@",
|
||||
keyboardResizes ? @"true" : @"false"];
|
||||
|
||||
return [[WKUserScript alloc] initWithSource:source
|
||||
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
|
||||
forMainFrameOnly:YES];
|
||||
}
|
||||
|
||||
- (WKUserScript *)autoCordovify
|
||||
{
|
||||
NSURL *cordovaURL = [[NSBundle mainBundle] URLForResource:@"www/cordova" withExtension:@"js"];
|
||||
if (cordovaURL == nil) {
|
||||
NSLog(@"CDVWKWebViewEngine: cordova.js WAS NOT FOUND");
|
||||
return nil;
|
||||
}
|
||||
NSError *error = nil;
|
||||
NSString *source = [NSString stringWithContentsOfURL:cordovaURL encoding:NSUTF8StringEncoding error:&error];
|
||||
if (source == nil || error != nil) {
|
||||
NSLog(@"CDVWKWebViewEngine: cordova.js can not be loaded: %@", error);
|
||||
return nil;
|
||||
}
|
||||
NSLog(@"CDVWKWebViewEngine: auto injecting cordova");
|
||||
NSString *cordovaPath = [self.CDV_LOCAL_SERVER stringByAppendingString:cordovaURL.URLByDeletingLastPathComponent.path];
|
||||
NSString *replacement = [NSString stringWithFormat:@"var pathPrefix = '%@/';", cordovaPath];
|
||||
source = [source stringByReplacingOccurrencesOfString:@"var pathPrefix = findCordovaPath();" withString:replacement];
|
||||
|
||||
return [[WKUserScript alloc] initWithSource:source
|
||||
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
|
||||
forMainFrameOnly:YES];
|
||||
}
|
||||
|
||||
#pragma mark WKScriptMessageHandler implementation
|
||||
|
||||
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
|
||||
{
|
||||
if ([message.name isEqualToString:CDV_BRIDGE_NAME]) {
|
||||
[self handleCordovaMessage: message];
|
||||
} else if ([message.name isEqualToString:CDV_IONIC_STOP_SCROLL]) {
|
||||
[self handleStopScroll];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleCordovaMessage:(WKScriptMessage*)message
|
||||
{
|
||||
CDVViewController *vc = (CDVViewController*)self.viewController;
|
||||
|
||||
NSArray *jsonEntry = message.body; // NSString:callbackId, NSString:service, NSString:action, NSArray:args
|
||||
CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry];
|
||||
CDV_EXEC_LOG(@"Exec(%@): Calling %@.%@", command.callbackId, command.className, command.methodName);
|
||||
|
||||
if (![vc.commandQueue execute:command]) {
|
||||
#ifdef DEBUG
|
||||
NSError* error = nil;
|
||||
NSString* commandJson = nil;
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:jsonEntry
|
||||
options:0
|
||||
error:&error];
|
||||
|
||||
if (error == nil) {
|
||||
commandJson = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
static NSUInteger maxLogLength = 1024;
|
||||
NSString* commandString = ([commandJson length] > maxLogLength) ?
|
||||
[NSString stringWithFormat : @"%@[...]", [commandJson substringToIndex:maxLogLength]] :
|
||||
commandJson;
|
||||
|
||||
NSLog(@"FAILED pluginJSON = %@", commandString);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleStopScroll
|
||||
{
|
||||
WKWebView* wkWebView = (WKWebView*)_engineWebView;
|
||||
NSLog(@"CDVWKWebViewEngine: handleStopScroll");
|
||||
[self recursiveStopScroll:[wkWebView scrollView]];
|
||||
[wkWebView evaluateJavaScript:@"window.IonicStopScroll.fire()" completionHandler:nil];
|
||||
}
|
||||
|
||||
- (void)recursiveStopScroll:(UIView *)node
|
||||
{
|
||||
if([node isKindOfClass: [UIScrollView class]]) {
|
||||
UIScrollView *nodeAsScroll = (UIScrollView *)node;
|
||||
|
||||
if([nodeAsScroll isScrollEnabled] && ![nodeAsScroll isHidden]) {
|
||||
[nodeAsScroll setScrollEnabled: NO];
|
||||
[nodeAsScroll setScrollEnabled: YES];
|
||||
}
|
||||
}
|
||||
|
||||
// iterate tree recursivelly
|
||||
for (UIView *child in [node subviews]) {
|
||||
[self recursiveStopScroll:child];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark WKNavigationDelegate implementation
|
||||
|
||||
- (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(WKNavigation*)navigation
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginResetNotification object:webView]];
|
||||
}
|
||||
|
||||
- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation
|
||||
{
|
||||
#ifndef __CORDOVA_6_0_0
|
||||
CDVViewController* vc = (CDVViewController*)self.viewController;
|
||||
[CDVUserAgentUtil releaseLock:vc.userAgentLockToken];
|
||||
#endif
|
||||
[[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPageDidLoadNotification object:webView]];
|
||||
}
|
||||
|
||||
- (void)webView:(WKWebView*)theWebView didFailProvisionalNavigation:(WKNavigation*)navigation withError:(NSError*)error
|
||||
{
|
||||
[self webView:theWebView didFailNavigation:navigation withError:error];
|
||||
}
|
||||
|
||||
- (void)webView:(WKWebView*)theWebView didFailNavigation:(WKNavigation*)navigation withError:(NSError*)error
|
||||
{
|
||||
CDVViewController* vc = (CDVViewController*)self.viewController;
|
||||
#ifndef __CORDOVA_6_0_0
|
||||
[CDVUserAgentUtil releaseLock:vc.userAgentLockToken];
|
||||
#endif
|
||||
|
||||
NSString* message = [NSString stringWithFormat:@"Failed to load webpage with error: %@", [error localizedDescription]];
|
||||
NSLog(@"%@", message);
|
||||
|
||||
NSURL* errorUrl = vc.errorURL;
|
||||
if (errorUrl) {
|
||||
NSCharacterSet *charSet = [NSCharacterSet URLFragmentAllowedCharacterSet];
|
||||
errorUrl = [NSURL URLWithString:[NSString stringWithFormat:@"?error=%@", [message stringByAddingPercentEncodingWithAllowedCharacters:charSet]] relativeToURL:errorUrl];
|
||||
NSLog(@"%@", [errorUrl absoluteString]);
|
||||
[theWebView loadRequest:[NSURLRequest requestWithURL:errorUrl]];
|
||||
}
|
||||
#ifdef DEBUG
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:nil]];
|
||||
[vc presentViewController:alertController animated:YES completion:nil];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView
|
||||
{
|
||||
[webView reload];
|
||||
}
|
||||
|
||||
- (BOOL)defaultResourcePolicyForURL:(NSURL*)url
|
||||
{
|
||||
// all file:// urls are allowed
|
||||
if ([url isFileURL]) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void) webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
|
||||
{
|
||||
NSURL* url = [navigationAction.request URL];
|
||||
CDVViewController* vc = (CDVViewController*)self.viewController;
|
||||
|
||||
/*
|
||||
* Give plugins the chance to handle the url
|
||||
*/
|
||||
BOOL anyPluginsResponded = NO;
|
||||
BOOL shouldAllowRequest = NO;
|
||||
|
||||
for (NSString* pluginName in vc.pluginObjects) {
|
||||
CDVPlugin* plugin = [vc.pluginObjects objectForKey:pluginName];
|
||||
SEL selector = NSSelectorFromString(@"shouldOverrideLoadWithRequest:navigationType:");
|
||||
if ([plugin respondsToSelector:selector]) {
|
||||
anyPluginsResponded = YES;
|
||||
// https://issues.apache.org/jira/browse/CB-12497
|
||||
int navType = (int)navigationAction.navigationType;
|
||||
if (WKNavigationTypeOther == navigationAction.navigationType) {
|
||||
#ifdef __CORDOVA_6_0_0
|
||||
navType = -1;
|
||||
#else
|
||||
navType = 5;
|
||||
#endif
|
||||
}
|
||||
shouldAllowRequest = (((BOOL (*)(id, SEL, id, int))objc_msgSend)(plugin, selector, navigationAction.request, navType));
|
||||
if (!shouldAllowRequest) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyPluginsResponded) {
|
||||
/*
|
||||
* Handle all other types of urls (tel:, sms:), and requests to load a url in the main webview.
|
||||
*/
|
||||
shouldAllowRequest = [self defaultResourcePolicyForURL:url];
|
||||
if (!shouldAllowRequest) {
|
||||
[[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]];
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAllowRequest) {
|
||||
NSString *scheme = url.scheme;
|
||||
if ([scheme isEqualToString:@"tel"] ||
|
||||
[scheme isEqualToString:@"mailto"] ||
|
||||
[scheme isEqualToString:@"facetime"] ||
|
||||
[scheme isEqualToString:@"sms"] ||
|
||||
[scheme isEqualToString:@"maps"]) {
|
||||
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||||
decisionHandler(WKNavigationActionPolicyCancel);
|
||||
} else {
|
||||
decisionHandler(WKNavigationActionPolicyAllow);
|
||||
}
|
||||
} else {
|
||||
decisionHandler(WKNavigationActionPolicyCancel);
|
||||
}
|
||||
}
|
||||
|
||||
-(void)getServerBasePath:(CDVInvokedUrlCommand*)command
|
||||
{
|
||||
[self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:self.basePath] callbackId:command.callbackId];
|
||||
}
|
||||
|
||||
-(void)setServerBasePath:(CDVInvokedUrlCommand*)command
|
||||
{
|
||||
NSString * path = [command argumentAtIndex:0];
|
||||
self.basePath = path;
|
||||
[self.handler setAssetPath:path];
|
||||
|
||||
NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.CDV_LOCAL_SERVER]];
|
||||
[(WKWebView*)_engineWebView loadRequest:request];
|
||||
}
|
||||
|
||||
-(void)persistServerBasePath:(CDVInvokedUrlCommand*)command
|
||||
{
|
||||
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
|
||||
[userDefaults setObject:[self.basePath lastPathComponent] forKey:CDV_SERVER_PATH];
|
||||
[userDefaults synchronize];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - CDVWKWeakScriptMessageHandler
|
||||
|
||||
@implementation CDVWKWeakScriptMessageHandler
|
||||
|
||||
- (instancetype)initWithScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_scriptMessageHandler = scriptMessageHandler;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
|
||||
{
|
||||
[self.scriptMessageHandler userContentController:userContentController didReceiveScriptMessage:message];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you 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.
|
||||
*/
|
||||
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
@interface CDVWKWebViewUIDelegate : NSObject <WKUIDelegate>
|
||||
|
||||
@property (nonatomic, copy) NSString* title;
|
||||
|
||||
- (instancetype)initWithTitle:(NSString*)title;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you 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.
|
||||
*/
|
||||
|
||||
#import "CDVWKWebViewUIDelegate.h"
|
||||
|
||||
@implementation CDVWKWebViewUIDelegate
|
||||
|
||||
- (instancetype)initWithTitle:(NSString*)title
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.title = title;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) webView:(WKWebView*)webView runJavaScriptAlertPanelWithMessage:(NSString*)message
|
||||
initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(void))completionHandler
|
||||
{
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction* action)
|
||||
{
|
||||
completionHandler();
|
||||
[alert dismissViewControllerAnimated:YES completion:nil];
|
||||
}];
|
||||
|
||||
[alert addAction:ok];
|
||||
|
||||
UIViewController* rootController = [UIApplication sharedApplication].delegate.window.rootViewController;
|
||||
|
||||
[rootController presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void) webView:(WKWebView*)webView runJavaScriptConfirmPanelWithMessage:(NSString*)message
|
||||
initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(BOOL result))completionHandler
|
||||
{
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction* action)
|
||||
{
|
||||
completionHandler(YES);
|
||||
[alert dismissViewControllerAnimated:YES completion:nil];
|
||||
}];
|
||||
|
||||
[alert addAction:ok];
|
||||
|
||||
UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction* action)
|
||||
{
|
||||
completionHandler(NO);
|
||||
[alert dismissViewControllerAnimated:YES completion:nil];
|
||||
}];
|
||||
[alert addAction:cancel];
|
||||
|
||||
UIViewController* rootController = [UIApplication sharedApplication].delegate.window.rootViewController;
|
||||
|
||||
[rootController presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void) webView:(WKWebView*)webView runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt
|
||||
defaultText:(NSString*)defaultText initiatedByFrame:(WKFrameInfo*)frame
|
||||
completionHandler:(void (^)(NSString* result))completionHandler
|
||||
{
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title
|
||||
message:prompt
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction* action)
|
||||
{
|
||||
completionHandler(((UITextField*)alert.textFields[0]).text);
|
||||
[alert dismissViewControllerAnimated:YES completion:nil];
|
||||
}];
|
||||
|
||||
[alert addAction:ok];
|
||||
|
||||
UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction* action)
|
||||
{
|
||||
completionHandler(nil);
|
||||
[alert dismissViewControllerAnimated:YES completion:nil];
|
||||
}];
|
||||
[alert addAction:cancel];
|
||||
|
||||
[alert addTextFieldWithConfigurationHandler:^(UITextField* textField) {
|
||||
textField.text = defaultText;
|
||||
}];
|
||||
|
||||
UIViewController* rootController = [UIApplication sharedApplication].delegate.window.rootViewController;
|
||||
|
||||
[rootController presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
@interface IONAssetHandler : NSObject <WKURLSchemeHandler>
|
||||
|
||||
@property (nonatomic, strong) NSString * basePath;
|
||||
@property (nonatomic, strong) NSString * scheme;
|
||||
|
||||
-(void)setAssetPath:(NSString *)assetPath;
|
||||
- (instancetype)initWithBasePath:(NSString *)basePath andScheme:(NSString *)scheme;
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,92 @@
|
||||
#import "IONAssetHandler.h"
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import "CDVWKWebViewEngine.h"
|
||||
|
||||
@implementation IONAssetHandler
|
||||
|
||||
-(void)setAssetPath:(NSString *)assetPath {
|
||||
self.basePath = assetPath;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBasePath:(NSString *)basePath andScheme:(NSString *)scheme {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_basePath = basePath;
|
||||
_scheme = scheme;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
|
||||
{
|
||||
NSString * startPath = @"";
|
||||
NSURL * url = urlSchemeTask.request.URL;
|
||||
NSString * stringToLoad = url.path;
|
||||
NSString * scheme = url.scheme;
|
||||
|
||||
if ([scheme isEqualToString:self.scheme]) {
|
||||
if ([stringToLoad hasPrefix:@"/_app_file_"]) {
|
||||
startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""];
|
||||
} else {
|
||||
startPath = self.basePath ? self.basePath : @"";
|
||||
if ([stringToLoad isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) {
|
||||
startPath = [startPath stringByAppendingString:@"/index.html"];
|
||||
} else {
|
||||
startPath = [startPath stringByAppendingString:stringToLoad];
|
||||
}
|
||||
}
|
||||
}
|
||||
NSError * fileError = nil;
|
||||
NSData * data = nil;
|
||||
if ([self isMediaExtension:url.pathExtension]) {
|
||||
data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError];
|
||||
}
|
||||
if (!data || fileError) {
|
||||
data = [[NSData alloc] initWithContentsOfFile:startPath];
|
||||
}
|
||||
NSInteger statusCode = 200;
|
||||
if (!data) {
|
||||
statusCode = 404;
|
||||
}
|
||||
NSURL * localUrl = [NSURL URLWithString:url.absoluteString];
|
||||
NSString * mimeType = [self getMimeType:url.pathExtension];
|
||||
id response = nil;
|
||||
if (data && [self isMediaExtension:url.pathExtension]) {
|
||||
response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
|
||||
} else {
|
||||
NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"};
|
||||
response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers];
|
||||
}
|
||||
|
||||
[urlSchemeTask didReceiveResponse:response];
|
||||
[urlSchemeTask didReceiveData:data];
|
||||
[urlSchemeTask didFinish];
|
||||
|
||||
}
|
||||
|
||||
- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask
|
||||
{
|
||||
NSLog(@"stop");
|
||||
}
|
||||
|
||||
-(NSString *) getMimeType:(NSString *)fileExtension {
|
||||
if (fileExtension && ![fileExtension isEqualToString:@""]) {
|
||||
NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL);
|
||||
NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
|
||||
return contentType ? contentType : @"application/octet-stream";
|
||||
} else {
|
||||
return @"text/html";
|
||||
}
|
||||
}
|
||||
|
||||
-(BOOL) isMediaExtension:(NSString *) pathExtension {
|
||||
NSArray * mediaExtensions = @[@"m4v", @"mov", @"mp4",
|
||||
@"aac", @"ac3", @"aiff", @"au", @"flac", @"m4a", @"mp3", @"wav"];
|
||||
if ([mediaExtensions containsObject:pathExtension.lowercaseString]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2012 Niklas von Hertzen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
(function _wk_plugin() {
|
||||
// Check if we are running in WKWebView
|
||||
if (!window.webkit || !window.webkit.messageHandlers) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Ionic
|
||||
window.Ionic = window.Ionic || {};
|
||||
|
||||
function normalizeURL(url) {
|
||||
console.warn('normalizeURL is deprecated, use window.Ionic.WebView.convertFileSrc');
|
||||
return window.Ionic.WebView.convertFileSrc(url);
|
||||
}
|
||||
if (typeof window.wkRewriteURL === 'undefined') {
|
||||
window.wkRewriteURL = function (url) {
|
||||
console.warn('wkRewriteURL is deprecated, use window.Ionic.WebView.convertFileSrc instead');
|
||||
return window.Ionic.WebView.convertFileSrc(url);
|
||||
}
|
||||
}
|
||||
window.Ionic.normalizeURL = normalizeURL;
|
||||
|
||||
var stopScrollHandler = window.webkit.messageHandlers.stopScroll;
|
||||
if (!stopScrollHandler) {
|
||||
console.error('Can not find stopScroll handler');
|
||||
return;
|
||||
}
|
||||
|
||||
var stopScrollFunc = null;
|
||||
var stopScroll = {
|
||||
stop: function stop(callback) {
|
||||
if (!stopScrollFunc) {
|
||||
stopScrollFunc = callback;
|
||||
stopScrollHandler.postMessage('');
|
||||
}
|
||||
},
|
||||
fire: function fire() {
|
||||
stopScrollFunc && stopScrollFunc();
|
||||
stopScrollFunc = null;
|
||||
},
|
||||
cancel: function cancel() {
|
||||
stopScrollFunc = null;
|
||||
}
|
||||
};
|
||||
|
||||
window.Ionic.StopScroll = stopScroll;
|
||||
// deprecated
|
||||
window.IonicStopScroll = stopScroll;
|
||||
|
||||
console.debug("Ionic Stop Scroll injected!");
|
||||
})();
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates the exec bridge used to notify the native code of
|
||||
* commands.
|
||||
*/
|
||||
var cordova = require('cordova');
|
||||
var utils = require('cordova/utils');
|
||||
var base64 = require('cordova/base64');
|
||||
|
||||
function massageArgsJsToNative (args) {
|
||||
if (!args || utils.typeName(args) !== 'Array') {
|
||||
return args;
|
||||
}
|
||||
var ret = [];
|
||||
args.forEach(function (arg, i) {
|
||||
if (utils.typeName(arg) === 'ArrayBuffer') {
|
||||
ret.push({
|
||||
'CDVType': 'ArrayBuffer',
|
||||
'data': base64.fromArrayBuffer(arg)
|
||||
});
|
||||
} else {
|
||||
ret.push(arg);
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
function massageMessageNativeToJs (message) {
|
||||
if (message.CDVType === 'ArrayBuffer') {
|
||||
var stringToArrayBuffer = function (str) {
|
||||
var ret = new Uint8Array(str.length);
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
ret[i] = str.charCodeAt(i);
|
||||
}
|
||||
return ret.buffer;
|
||||
};
|
||||
var base64ToArrayBuffer = function (b64) {
|
||||
return stringToArrayBuffer(atob(b64)); // eslint-disable-line no-undef
|
||||
};
|
||||
message = base64ToArrayBuffer(message.data);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function convertMessageToArgsNativeToJs (message) {
|
||||
var args = [];
|
||||
if (!message || !message.hasOwnProperty('CDVType')) {
|
||||
args.push(message);
|
||||
} else if (message.CDVType === 'MultiPart') {
|
||||
message.messages.forEach(function (e) {
|
||||
args.push(massageMessageNativeToJs(e));
|
||||
});
|
||||
} else {
|
||||
args.push(massageMessageNativeToJs(message));
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
var iOSExec = function () {
|
||||
// detect change in bridge, if there is a change, we forward to new bridge
|
||||
|
||||
// if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.cordova && window.webkit.messageHandlers.cordova.postMessage) {
|
||||
// bridgeMode = jsToNativeModes.WK_WEBVIEW_BINDING;
|
||||
// }
|
||||
|
||||
var successCallback, failCallback, service, action, actionArgs;
|
||||
var callbackId = null;
|
||||
if (typeof arguments[0] !== 'string') {
|
||||
// FORMAT ONE
|
||||
successCallback = arguments[0];
|
||||
failCallback = arguments[1];
|
||||
service = arguments[2];
|
||||
action = arguments[3];
|
||||
actionArgs = arguments[4];
|
||||
|
||||
// Since we need to maintain backwards compatibility, we have to pass
|
||||
// an invalid callbackId even if no callback was provided since plugins
|
||||
// will be expecting it. The Cordova.exec() implementation allocates
|
||||
// an invalid callbackId and passes it even if no callbacks were given.
|
||||
callbackId = 'INVALID';
|
||||
} else {
|
||||
throw new Error('The old format of this exec call has been removed (deprecated since 2.1). Change to: ' + // eslint-disable-line
|
||||
'cordova.exec(null, null, \'Service\', \'action\', [ arg1, arg2 ]);');
|
||||
}
|
||||
|
||||
// If actionArgs is not provided, default to an empty array
|
||||
actionArgs = actionArgs || [];
|
||||
|
||||
// Register the callbacks and add the callbackId to the positional
|
||||
// arguments if given.
|
||||
if (successCallback || failCallback) {
|
||||
callbackId = service + cordova.callbackId++;
|
||||
cordova.callbacks[callbackId] =
|
||||
{success: successCallback, fail: failCallback};
|
||||
}
|
||||
|
||||
actionArgs = massageArgsJsToNative(actionArgs);
|
||||
|
||||
// CB-10133 DataClone DOM Exception 25 guard (fast function remover)
|
||||
var command = [callbackId, service, action, JSON.parse(JSON.stringify(actionArgs))];
|
||||
window.webkit.messageHandlers.cordova.postMessage(command);
|
||||
};
|
||||
|
||||
iOSExec.nativeCallback = function (callbackId, status, message, keepCallback, debug) {
|
||||
var success = status === 0 || status === 1;
|
||||
var args = convertMessageToArgsNativeToJs(message);
|
||||
Promise.resolve().then(function () {
|
||||
cordova.callbackFromNative(callbackId, success, status, args, keepCallback); // eslint-disable-line
|
||||
});
|
||||
};
|
||||
|
||||
// for backwards compatibility
|
||||
iOSExec.nativeEvalAndFetch = function (func) {
|
||||
try {
|
||||
func();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Proxy the exec for bridge changes. See CB-10106
|
||||
|
||||
function cordovaExec () {
|
||||
var cexec = require('cordova/exec');
|
||||
var cexec_valid = (typeof cexec.nativeFetchMessages === 'function') && (typeof cexec.nativeEvalAndFetch === 'function') && (typeof cexec.nativeCallback === 'function');
|
||||
return (cexec_valid && execProxy !== cexec) ? cexec : iOSExec;
|
||||
}
|
||||
|
||||
function execProxy () {
|
||||
cordovaExec().apply(null, arguments);
|
||||
}
|
||||
|
||||
execProxy.nativeFetchMessages = function () {
|
||||
return cordovaExec().nativeFetchMessages.apply(null, arguments);
|
||||
};
|
||||
|
||||
execProxy.nativeEvalAndFetch = function () {
|
||||
return cordovaExec().nativeEvalAndFetch.apply(null, arguments);
|
||||
};
|
||||
|
||||
execProxy.nativeCallback = function () {
|
||||
return cordovaExec().nativeCallback.apply(null, arguments);
|
||||
};
|
||||
|
||||
module.exports = execProxy;
|
||||
|
||||
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.cordova && window.webkit.messageHandlers.cordova.postMessage) {
|
||||
// unregister the old bridge
|
||||
cordova.define.remove('cordova/exec');
|
||||
// redefine bridge to our new bridge
|
||||
cordova.define('cordova/exec', function (require, exports, module) {
|
||||
module.exports = execProxy;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
var exec = require('cordova/exec');
|
||||
|
||||
var WebView = {
|
||||
convertFileSrc: function(url) {
|
||||
if (!url) {
|
||||
return url;
|
||||
}
|
||||
if (url.indexOf('/')===0) {
|
||||
return window.WEBVIEW_SERVER_URL + '/_app_file_' + url;
|
||||
}
|
||||
if (url.indexOf('file://')===0) {
|
||||
return window.WEBVIEW_SERVER_URL + url.replace('file://', '/_app_file_');
|
||||
}
|
||||
if (url.indexOf('content://')===0) {
|
||||
return window.WEBVIEW_SERVER_URL + url.replace('content:/', '/_app_content_');
|
||||
}
|
||||
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', []);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebView;
|
||||
Reference in New Issue
Block a user