();
43 | mText = null;
44 | }
45 |
46 | /**
47 | * Add a URI to match, and the code to return when this URI is
48 | * matched. URI nodes may be exact match string, the token "*"
49 | * that matches any text, or the token "#" that matches only
50 | * numbers.
51 | *
52 | * Starting from API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
53 | * this method will accept a leading slash in the path.
54 | *
55 | * @param authority the authority to match
56 | * @param path the path to match. * may be used as a wild card for
57 | * any text, and # may be used as a wild card for numbers.
58 | * @param code the code that is returned when a URI is matched
59 | * against the given components. Must be positive.
60 | */
61 | public void addURI(String scheme, String authority, String path, Object code) {
62 | if (code == null) {
63 | throw new IllegalArgumentException("Code can't be null");
64 | }
65 |
66 | String[] tokens = null;
67 | if (path != null) {
68 | String newPath = path;
69 | // Strip leading slash if present.
70 | if (path.length() > 0 && path.charAt(0) == '/') {
71 | newPath = path.substring(1);
72 | }
73 | tokens = PATH_SPLIT_PATTERN.split(newPath);
74 | }
75 |
76 | int numTokens = tokens != null ? tokens.length : 0;
77 | UriMatcher node = this;
78 | for (int i = -2; i < numTokens; i++) {
79 | String token;
80 | if (i == -2)
81 | token = scheme;
82 | else if (i == -1)
83 | token = authority;
84 | else
85 | token = tokens[i];
86 | ArrayList children = node.mChildren;
87 | int numChildren = children.size();
88 | UriMatcher child;
89 | int j;
90 | for (j = 0; j < numChildren; j++) {
91 | child = children.get(j);
92 | if (token.equals(child.mText)) {
93 | node = child;
94 | break;
95 | }
96 | }
97 | if (j == numChildren) {
98 | // Child not found, create it
99 | child = new UriMatcher();
100 | if (token.equals("**")) {
101 | child.mWhich = REST;
102 | } else if (token.equals("*")) {
103 | child.mWhich = TEXT;
104 | } else {
105 | child.mWhich = EXACT;
106 | }
107 | child.mText = token;
108 | node.mChildren.add(child);
109 | node = child;
110 | }
111 | }
112 | node.mCode = code;
113 | }
114 |
115 | static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/");
116 |
117 | /**
118 | * Try to match against the path in a url.
119 | *
120 | * @param uri The url whose path we will match against.
121 | * @return The code for the matched node (added using addURI),
122 | * or null if there is no matched node.
123 | */
124 | public Object match(Uri uri) {
125 | final List pathSegments = uri.getPathSegments();
126 | final int li = pathSegments.size();
127 |
128 | UriMatcher node = this;
129 |
130 | if (li == 0 && uri.getAuthority() == null) {
131 | return this.mCode;
132 | }
133 |
134 | for (int i = -2; i < li; i++) {
135 | String u;
136 | if (i == -2)
137 | u = uri.getScheme();
138 | else if (i == -1)
139 | u = uri.getAuthority();
140 | else
141 | u = pathSegments.get(i);
142 | ArrayList list = node.mChildren;
143 | if (list == null) {
144 | break;
145 | }
146 | node = null;
147 | int lj = list.size();
148 | for (int j = 0; j < lj; j++) {
149 | UriMatcher n = list.get(j);
150 | which_switch:
151 | switch (n.mWhich) {
152 | case EXACT:
153 | if (n.mText.equals(u)) {
154 | node = n;
155 | }
156 | break;
157 | case TEXT:
158 | node = n;
159 | break;
160 | case REST:
161 | return n.mCode;
162 | }
163 | if (node != null) {
164 | break;
165 | }
166 | }
167 | if (node == null) {
168 | return null;
169 | }
170 | }
171 |
172 | return node.mCode;
173 | }
174 |
175 | private static final int EXACT = 0;
176 | private static final int TEXT = 1;
177 | private static final int REST = 2;
178 |
179 | private Object mCode;
180 | private int mWhich;
181 | private String mText;
182 | private ArrayList mChildren;
183 | }
184 |
--------------------------------------------------------------------------------
/src/android/com/ionicframework/cordova/webview/WebViewLocalServer.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2015 Google Inc. All rights reserved.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package com.ionicframework.cordova.webview;
17 |
18 | import android.content.Context;
19 | import android.net.Uri;
20 | import android.os.Build;
21 | import android.util.Log;
22 | import android.webkit.WebResourceRequest;
23 | import android.webkit.WebResourceResponse;
24 |
25 | import org.apache.cordova.ConfigXmlParser;
26 |
27 | import java.io.IOException;
28 | import java.io.InputStream;
29 | import java.net.HttpURLConnection;
30 | import java.net.SocketTimeoutException;
31 | import java.net.URL;
32 | import java.net.URLConnection;
33 | import java.util.HashMap;
34 | import java.util.Map;
35 | import java.util.UUID;
36 |
37 | /**
38 | * Helper class meant to be used with the android.webkit.WebView class to enable hosting assets,
39 | * resources and other data on 'virtual' http(s):// URL.
40 | * Hosting assets and resources on http(s):// URLs is desirable as it is compatible with the
41 | * Same-Origin policy.
42 | *
43 | * This class is intended to be used from within the
44 | * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, String)} and
45 | * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
46 | * android.webkit.WebResourceRequest)}
47 | * methods.
48 | */
49 | public class WebViewLocalServer {
50 | private static String TAG = "WebViewAssetServer";
51 | private String basePath;
52 | public final static String httpScheme = "http";
53 | public final static String httpsScheme = "https";
54 | public final static String fileStart = "/_app_file_";
55 | public final static String contentStart = "/_app_content_";
56 |
57 | private final UriMatcher uriMatcher;
58 | private final AndroidProtocolHandler protocolHandler;
59 | private final String authority;
60 | private final String customScheme;
61 | // Whether we're serving local files or proxying (for example, when doing livereload on a
62 | // non-local endpoint (will be false in that case)
63 | private boolean isAsset;
64 | // Whether to route all requests to paths without extensions back to `index.html`
65 | private final boolean html5mode;
66 | private ConfigXmlParser parser;
67 |
68 | public String getAuthority() { return authority; }
69 |
70 | /**
71 | * A handler that produces responses for paths on the virtual asset server.
72 | *
73 | * Methods of this handler will be invoked on a background thread and care must be taken to
74 | * correctly synchronize access to any shared state.
75 | *
76 | * On Android KitKat and above these methods may be called on more than one thread. This thread
77 | * may be different than the thread on which the shouldInterceptRequest method was invoke.
78 | * This means that on Android KitKat and above it is possible to block in this method without
79 | * blocking other resources from loading. The number of threads used to parallelize loading
80 | * is an internal implementation detail of the WebView and may change between updates which
81 | * means that the amount of time spend blocking in this method should be kept to an absolute
82 | * minimum.
83 | */
84 | public abstract static class PathHandler {
85 | protected String mimeType;
86 | private String encoding;
87 | private String charset;
88 | private int statusCode;
89 | private String reasonPhrase;
90 | private Map responseHeaders;
91 |
92 | public PathHandler() {
93 | this(null, null, 200, "OK", null);
94 | }
95 |
96 | public PathHandler(String encoding, String charset, int statusCode,
97 | String reasonPhrase, Map responseHeaders) {
98 | this.encoding = encoding;
99 | this.charset = charset;
100 | this.statusCode = statusCode;
101 | this.reasonPhrase = reasonPhrase;
102 | Map tempResponseHeaders;
103 | if (responseHeaders == null) {
104 | tempResponseHeaders = new HashMap();
105 | } else {
106 | tempResponseHeaders = responseHeaders;
107 | }
108 | tempResponseHeaders.put("Cache-Control", "no-cache");
109 | this.responseHeaders = tempResponseHeaders;
110 | }
111 |
112 | abstract public InputStream handle(Uri url);
113 |
114 | public String getEncoding() {
115 | return encoding;
116 | }
117 |
118 | public String getCharset() {
119 | return charset;
120 | }
121 |
122 | public int getStatusCode() {
123 | return statusCode;
124 | }
125 |
126 | public String getReasonPhrase() {
127 | return reasonPhrase;
128 | }
129 |
130 | public Map getResponseHeaders() {
131 | return responseHeaders;
132 | }
133 | }
134 |
135 | /**
136 | * Information about the URLs used to host the assets in the WebView.
137 | */
138 | public static class AssetHostingDetails {
139 | private Uri httpPrefix;
140 | private Uri httpsPrefix;
141 |
142 | /*package*/ AssetHostingDetails(Uri httpPrefix, Uri httpsPrefix) {
143 | this.httpPrefix = httpPrefix;
144 | this.httpsPrefix = httpsPrefix;
145 | }
146 |
147 | /**
148 | * Gets the http: scheme prefix at which assets are hosted.
149 | *
150 | * @return the http: scheme prefix at which assets are hosted. Can return null.
151 | */
152 | public Uri getHttpPrefix() {
153 | return httpPrefix;
154 | }
155 |
156 | /**
157 | * Gets the https: scheme prefix at which assets are hosted.
158 | *
159 | * @return the https: scheme prefix at which assets are hosted. Can return null.
160 | */
161 | public Uri getHttpsPrefix() {
162 | return httpsPrefix;
163 | }
164 | }
165 |
166 | WebViewLocalServer(Context context, String authority, boolean html5mode, ConfigXmlParser parser, String customScheme) {
167 | uriMatcher = new UriMatcher(null);
168 | this.html5mode = html5mode;
169 | this.parser = parser;
170 | this.protocolHandler = new AndroidProtocolHandler(context.getApplicationContext());
171 | this.authority = authority;
172 | this.customScheme = customScheme;
173 | }
174 |
175 | private static Uri parseAndVerifyUrl(String url) {
176 | if (url == null) {
177 | return null;
178 | }
179 | Uri uri = Uri.parse(url);
180 | if (uri == null) {
181 | Log.e(TAG, "Malformed URL: " + url);
182 | return null;
183 | }
184 | String path = uri.getPath();
185 | if (path == null || path.length() == 0) {
186 | Log.e(TAG, "URL does not have a path: " + url);
187 | return null;
188 | }
189 | return uri;
190 | }
191 |
192 | private static WebResourceResponse createWebResourceResponse(String mimeType, String encoding, int statusCode, String reasonPhrase, Map responseHeaders, InputStream data) {
193 | if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
194 | int finalStatusCode = statusCode;
195 | try {
196 | if (data.available() == 0) {
197 | finalStatusCode = 404;
198 | }
199 | } catch (IOException e) {
200 | finalStatusCode = 500;
201 | }
202 | return new WebResourceResponse(mimeType, encoding, finalStatusCode, reasonPhrase, responseHeaders, data);
203 | } else {
204 | return new WebResourceResponse(mimeType, encoding, data);
205 | }
206 | }
207 |
208 | /**
209 | * Attempt to retrieve the WebResourceResponse associated with the given request
.
210 | * This method should be invoked from within
211 | * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
212 | * android.webkit.WebResourceRequest)}.
213 | *
214 | * @param uri the request Uri to process.
215 | * @return a response if the request URL had a matching handler, null if no handler was found.
216 | */
217 | public WebResourceResponse shouldInterceptRequest(Uri uri, WebResourceRequest request) {
218 | PathHandler handler;
219 | synchronized (uriMatcher) {
220 | handler = (PathHandler) uriMatcher.match(uri);
221 | }
222 | if (handler == null) {
223 | return null;
224 | }
225 |
226 | if (isLocalFile(uri) || uri.getAuthority().equals(this.authority)) {
227 | Log.d("SERVER", "Handling local request: " + uri.toString());
228 | return handleLocalRequest(uri, handler, request);
229 | } else {
230 | return handleProxyRequest(uri, handler);
231 | }
232 | }
233 |
234 | private boolean isLocalFile(Uri uri) {
235 | String path = uri.getPath();
236 | if (path.startsWith(contentStart) || path.startsWith(fileStart)) {
237 | return true;
238 | }
239 | return false;
240 | }
241 |
242 |
243 | private WebResourceResponse handleLocalRequest(Uri uri, PathHandler handler, WebResourceRequest request) {
244 | String path = uri.getPath();
245 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && request != null && request.getRequestHeaders().get("Range") != null) {
246 | InputStream responseStream = new LollipopLazyInputStream(handler, uri);
247 | String mimeType = getMimeType(path, responseStream);
248 | Map tempResponseHeaders = handler.getResponseHeaders();
249 | int statusCode = 206;
250 | try {
251 | int totalRange = responseStream.available();
252 | String rangeString = request.getRequestHeaders().get("Range");
253 | String[] parts = rangeString.split("=");
254 | String[] streamParts = parts[1].split("-");
255 | String fromRange = streamParts[0];
256 | int range = totalRange-1;
257 | if (streamParts.length > 1) {
258 | range = Integer.parseInt(streamParts[1]);
259 | }
260 | tempResponseHeaders.put("Accept-Ranges", "bytes");
261 | tempResponseHeaders.put("Content-Range", "bytes " + fromRange + "-" + range + "/" + totalRange);
262 | } catch (IOException e) {
263 | statusCode = 404;
264 | }
265 | return createWebResourceResponse(mimeType, handler.getEncoding(),
266 | statusCode, handler.getReasonPhrase(), tempResponseHeaders, responseStream);
267 | }
268 | if (isLocalFile(uri)) {
269 | InputStream responseStream = new LollipopLazyInputStream(handler, uri);
270 | String mimeType = getMimeType(path, responseStream);
271 | return createWebResourceResponse(mimeType, handler.getEncoding(),
272 | handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), responseStream);
273 | }
274 |
275 | if (path.equals("") || path.equals("/") || (!uri.getLastPathSegment().contains(".") && html5mode)) {
276 | InputStream stream;
277 | String launchURL = parser.getLaunchUrl();
278 | String launchFile = launchURL.substring(launchURL.lastIndexOf("/") + 1, launchURL.length());
279 | try {
280 | String startPath = this.basePath + "/" + launchFile;
281 | if (isAsset) {
282 | stream = protocolHandler.openAsset(startPath);
283 | } else {
284 | stream = protocolHandler.openFile(startPath);
285 | }
286 |
287 | } catch (IOException e) {
288 | Log.e(TAG, "Unable to open " + launchFile, e);
289 | return null;
290 | }
291 |
292 | return createWebResourceResponse("text/html", handler.getEncoding(),
293 | handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream);
294 | }
295 |
296 | int periodIndex = path.lastIndexOf(".");
297 | if (periodIndex >= 0) {
298 | InputStream responseStream = new LollipopLazyInputStream(handler, uri);
299 | String mimeType = getMimeType(path, responseStream);
300 | return createWebResourceResponse(mimeType, handler.getEncoding(),
301 | handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), responseStream);
302 | }
303 |
304 | return null;
305 | }
306 |
307 | /**
308 | * Instead of reading files from the filesystem/assets, proxy through to the URL
309 | * and let an external server handle it.
310 | * @param uri
311 | * @param handler
312 | * @return
313 | */
314 | private WebResourceResponse handleProxyRequest(Uri uri, PathHandler handler) {
315 | try {
316 | String path = uri.getPath();
317 | URL url = new URL(uri.toString());
318 | HttpURLConnection conn = (HttpURLConnection) url.openConnection();
319 | conn.setRequestMethod("GET");
320 | conn.setReadTimeout(30 * 1000);
321 | conn.setConnectTimeout(30 * 1000);
322 |
323 | InputStream stream = conn.getInputStream();
324 |
325 | if (path.equals("/") || (!uri.getLastPathSegment().contains(".") && html5mode)) {
326 | return createWebResourceResponse("text/html", handler.getEncoding(),
327 | handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream);
328 | }
329 |
330 | int periodIndex = path.lastIndexOf(".");
331 | if (periodIndex >= 0) {
332 | String ext = path.substring(path.lastIndexOf("."), path.length());
333 |
334 | // TODO: Conjure up a bit more subtlety than this
335 | if (ext.equals(".html")) {
336 | }
337 |
338 | String mimeType = getMimeType(path, stream);
339 |
340 | return createWebResourceResponse(mimeType, handler.getEncoding(),
341 | handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream);
342 | }
343 |
344 | return createWebResourceResponse("", handler.getEncoding(),
345 | handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream);
346 |
347 | } catch (SocketTimeoutException ex) {
348 | // bridge.handleAppUrlLoadError(ex);
349 | } catch (Exception ex) {
350 | // bridge.handleAppUrlLoadError(ex);
351 | }
352 | return null;
353 | }
354 |
355 | private String getMimeType(String path, InputStream stream) {
356 | String mimeType = null;
357 | try {
358 | mimeType = URLConnection.guessContentTypeFromName(path); // Does not recognize *.js
359 | if (mimeType != null && path.endsWith(".js") && mimeType.equals("image/x-icon")) {
360 | Log.d(IonicWebViewEngine.TAG, "We shouldn't be here");
361 | }
362 | if (mimeType == null) {
363 | if (path.endsWith(".js") || path.endsWith(".mjs")) {
364 | // Make sure JS files get the proper mimetype to support ES modules
365 | mimeType = "application/javascript";
366 | } else if (path.endsWith(".wasm")) {
367 | mimeType = "application/wasm";
368 | } else {
369 | mimeType = URLConnection.guessContentTypeFromStream(stream);
370 | }
371 | }
372 | } catch (Exception ex) {
373 | Log.e(TAG, "Unable to get mime type" + path, ex);
374 | }
375 | return mimeType;
376 | }
377 |
378 | /**
379 | * Registers a handler for the given uri
. The handler
will be invoked
380 | * every time the shouldInterceptRequest
method of the instance is called with
381 | * a matching uri
.
382 | *
383 | * @param uri the uri to use the handler for. The scheme and authority (domain) will be matched
384 | * exactly. The path may contain a '*' element which will match a single element of
385 | * a path (so a handler registered for /a/* will be invoked for /a/b and /a/c.html
386 | * but not for /a/b/b) or the '**' element which will match any number of path
387 | * elements.
388 | * @param handler the handler to use for the uri.
389 | */
390 | void register(Uri uri, PathHandler handler) {
391 | synchronized (uriMatcher) {
392 | uriMatcher.addURI(uri.getScheme(), uri.getAuthority(), uri.getPath(), handler);
393 | }
394 | }
395 |
396 | /**
397 | * Hosts the application's assets on an http(s):// URL. Assets from the local path
398 | * assetPath/...
will be available under
399 | * http(s)://{uuid}.androidplatform.net/assets/...
.
400 | *
401 | * @param assetPath the local path in the application's asset folder which will be made
402 | * available by the server (for example "/www").
403 | */
404 | public void hostAssets(String assetPath) {
405 | hostAssets(authority, assetPath);
406 | }
407 |
408 |
409 | /**
410 | * Hosts the application's assets on an http(s):// URL. Assets from the local path
411 | * assetPath/...
will be available under
412 | * http(s)://{domain}/{virtualAssetPath}/...
.
413 | *
414 | * @param domain custom domain on which the assets should be hosted (for example "example.com").
415 | * @param assetPath the local path in the application's asset folder which will be made
416 | * available by the server (for example "/www").
417 | * @return prefixes under which the assets are hosted.
418 | */
419 | public void hostAssets(final String domain,
420 | final String assetPath) {
421 | this.isAsset = true;
422 | this.basePath = assetPath;
423 |
424 | createHostingDetails();
425 | }
426 |
427 | private void createHostingDetails() {
428 | final String assetPath = this.basePath;
429 |
430 | if (assetPath.indexOf('*') != -1) {
431 | throw new IllegalArgumentException("assetPath cannot contain the '*' character.");
432 | }
433 |
434 | PathHandler handler = new PathHandler() {
435 | @Override
436 | public InputStream handle(Uri url) {
437 | InputStream stream = null;
438 | String path = url.getPath();
439 | try {
440 | if (path.startsWith(contentStart)) {
441 | stream = protocolHandler.openContentUrl(url);
442 | } else if (path.startsWith(fileStart) || !isAsset) {
443 | if (!path.startsWith(fileStart)) {
444 | path = basePath + url.getPath();
445 | }
446 | stream = protocolHandler.openFile(path);
447 | } else {
448 | stream = protocolHandler.openAsset(assetPath + path);
449 | }
450 | } catch (IOException e) {
451 | Log.e(TAG, "Unable to open asset URL: " + url);
452 | return null;
453 | }
454 |
455 | return stream;
456 | }
457 | };
458 |
459 | registerUriForScheme(httpScheme, handler, authority);
460 | registerUriForScheme(httpsScheme, handler, authority);
461 | if (!customScheme.equals(httpScheme) && !customScheme.equals(httpsScheme)) {
462 | registerUriForScheme(customScheme, handler, authority);
463 | }
464 |
465 | }
466 |
467 | private void registerUriForScheme(String scheme, PathHandler handler, String authority) {
468 | Uri.Builder uriBuilder = new Uri.Builder();
469 | uriBuilder.scheme(scheme);
470 | uriBuilder.authority(authority);
471 | uriBuilder.path("");
472 | Uri uriPrefix = uriBuilder.build();
473 |
474 | register(Uri.withAppendedPath(uriPrefix, "/"), handler);
475 | register(Uri.withAppendedPath(uriPrefix, "**"), handler);
476 | }
477 |
478 | /**
479 | * Hosts the application's resources on an http(s):// URL. Resources
480 | * http(s)://{uuid}.androidplatform.net/res/{resource_type}/{resource_name}
.
481 | *
482 | * @return prefixes under which the resources are hosted.
483 | */
484 | public AssetHostingDetails hostResources() {
485 | return hostResources(authority, "/res", true, true);
486 | }
487 |
488 | /**
489 | * Hosts the application's resources on an http(s):// URL. Resources
490 | * http(s)://{uuid}.androidplatform.net/{virtualResourcesPath}/{resource_type}/{resource_name}
.
491 | *
492 | * @param virtualResourcesPath the path on the local server under which the resources
493 | * should be hosted.
494 | * @param enableHttp whether to enable hosting using the http scheme.
495 | * @param enableHttps whether to enable hosting using the https scheme.
496 | * @return prefixes under which the resources are hosted.
497 | */
498 | public AssetHostingDetails hostResources(final String virtualResourcesPath, boolean enableHttp,
499 | boolean enableHttps) {
500 | return hostResources(authority, virtualResourcesPath, enableHttp, enableHttps);
501 | }
502 |
503 | /**
504 | * Hosts the application's resources on an http(s):// URL. Resources
505 | * http(s)://{domain}/{virtualResourcesPath}/{resource_type}/{resource_name}
.
506 | *
507 | * @param domain custom domain on which the assets should be hosted (for example "example.com").
508 | * If untrusted content is to be loaded into the WebView it is advised to make
509 | * this random.
510 | * @param virtualResourcesPath the path on the local server under which the resources
511 | * should be hosted.
512 | * @param enableHttp whether to enable hosting using the http scheme.
513 | * @param enableHttps whether to enable hosting using the https scheme.
514 | * @return prefixes under which the resources are hosted.
515 | */
516 | public AssetHostingDetails hostResources(final String domain,
517 | final String virtualResourcesPath, boolean enableHttp,
518 | boolean enableHttps) {
519 | if (virtualResourcesPath.indexOf('*') != -1) {
520 | throw new IllegalArgumentException(
521 | "virtualResourcesPath cannot contain the '*' character.");
522 | }
523 |
524 | Uri.Builder uriBuilder = new Uri.Builder();
525 | uriBuilder.scheme(httpScheme);
526 | uriBuilder.authority(domain);
527 | uriBuilder.path(virtualResourcesPath);
528 |
529 | Uri httpPrefix = null;
530 | Uri httpsPrefix = null;
531 |
532 | PathHandler handler = new PathHandler() {
533 | @Override
534 | public InputStream handle(Uri url) {
535 | InputStream stream = protocolHandler.openResource(url);
536 | String mimeType = null;
537 | try {
538 | mimeType = URLConnection.guessContentTypeFromStream(stream);
539 | } catch (Exception ex) {
540 | Log.e(TAG, "Unable to get mime type" + url);
541 | }
542 |
543 | return stream;
544 | }
545 | };
546 |
547 | if (enableHttp) {
548 | httpPrefix = uriBuilder.build();
549 | register(Uri.withAppendedPath(httpPrefix, "**"), handler);
550 | }
551 | if (enableHttps) {
552 | uriBuilder.scheme(httpsScheme);
553 | httpsPrefix = uriBuilder.build();
554 | register(Uri.withAppendedPath(httpsPrefix, "**"), handler);
555 | }
556 | return new AssetHostingDetails(httpPrefix, httpsPrefix);
557 | }
558 |
559 |
560 | /**
561 | * Hosts the application's files on an http(s):// URL. Files from the basePath
562 | * basePath/...
will be available under
563 | * http(s)://{uuid}.androidplatform.net/...
.
564 | *
565 | * @param basePath the local path in the application's data folder which will be made
566 | * available by the server (for example "/www").
567 | */
568 | public void hostFiles(final String basePath) {
569 | this.isAsset = false;
570 | this.basePath = basePath;
571 | createHostingDetails();
572 | }
573 |
574 | /**
575 | * The KitKat WebView reads the InputStream on a separate threadpool. We can use that to
576 | * parallelize loading.
577 | */
578 | private static abstract class LazyInputStream extends InputStream {
579 | protected final PathHandler handler;
580 | private InputStream is = null;
581 |
582 | public LazyInputStream(PathHandler handler) {
583 | this.handler = handler;
584 | }
585 |
586 | private InputStream getInputStream() {
587 | if (is == null) {
588 | is = handle();
589 | }
590 | return is;
591 | }
592 |
593 | protected abstract InputStream handle();
594 |
595 | @Override
596 | public int available() throws IOException {
597 | InputStream is = getInputStream();
598 | return (is != null) ? is.available() : 0;
599 | }
600 |
601 | @Override
602 | public int read() throws IOException {
603 | InputStream is = getInputStream();
604 | return (is != null) ? is.read() : -1;
605 | }
606 |
607 | @Override
608 | public int read(byte b[]) throws IOException {
609 | InputStream is = getInputStream();
610 | return (is != null) ? is.read(b) : -1;
611 | }
612 |
613 | @Override
614 | public int read(byte b[], int off, int len) throws IOException {
615 | InputStream is = getInputStream();
616 | return (is != null) ? is.read(b, off, len) : -1;
617 | }
618 |
619 | @Override
620 | public long skip(long n) throws IOException {
621 | InputStream is = getInputStream();
622 | return (is != null) ? is.skip(n) : 0;
623 | }
624 | }
625 |
626 | // For L and above.
627 | private static class LollipopLazyInputStream extends LazyInputStream {
628 | private Uri uri;
629 | private InputStream is;
630 |
631 | public LollipopLazyInputStream(PathHandler handler, Uri uri) {
632 | super(handler);
633 | this.uri = uri;
634 | }
635 |
636 | @Override
637 | protected InputStream handle() {
638 | return handler.handle(uri);
639 | }
640 | }
641 |
642 | public String getBasePath(){
643 | return this.basePath;
644 | }
645 | }
646 |
--------------------------------------------------------------------------------
/src/ios/CDVWKProcessPoolFactory.h:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed to the Apache Software Foundation (ASF) under one
3 | or more contributor license agreements. See the NOTICE file
4 | distributed with this work for additional information
5 | regarding copyright ownership. The ASF licenses this file
6 | to you under the Apache License, Version 2.0 (the
7 | "License"); you may not use this file except in compliance
8 | with the License. You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing,
13 | software distributed under the License is distributed on an
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | KIND, either express or implied. See the License for the
16 | specific language governing permissions and limitations
17 | under the License.
18 | */
19 |
20 | #import
21 |
22 | @interface CDVWKProcessPoolFactory : NSObject
23 | @property (nonatomic, retain) WKProcessPool* sharedPool;
24 |
25 | +(instancetype) sharedFactory;
26 | -(WKProcessPool*) sharedProcessPool;
27 | @end
28 |
--------------------------------------------------------------------------------
/src/ios/CDVWKProcessPoolFactory.m:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed to the Apache Software Foundation (ASF) under one
3 | or more contributor license agreements. See the NOTICE file
4 | distributed with this work for additional information
5 | regarding copyright ownership. The ASF licenses this file
6 | to you under the Apache License, Version 2.0 (the
7 | "License"); you may not use this file except in compliance
8 | with the License. You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing,
13 | software distributed under the License is distributed on an
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | KIND, either express or implied. See the License for the
16 | specific language governing permissions and limitations
17 | under the License.
18 | */
19 |
20 | #import
21 | #import
22 | #import "CDVWKProcessPoolFactory.h"
23 |
24 | static CDVWKProcessPoolFactory *factory = nil;
25 |
26 | @implementation CDVWKProcessPoolFactory
27 |
28 | + (instancetype)sharedFactory
29 | {
30 | static dispatch_once_t onceToken;
31 | dispatch_once(&onceToken, ^{
32 | factory = [[CDVWKProcessPoolFactory alloc] init];
33 | });
34 |
35 | return factory;
36 | }
37 |
38 | - (instancetype)init
39 | {
40 | if (self = [super init]) {
41 | _sharedPool = [[WKProcessPool alloc] init];
42 | }
43 | return self;
44 | }
45 |
46 | - (WKProcessPool*) sharedProcessPool {
47 | return _sharedPool;
48 | }
49 | @end
50 |
--------------------------------------------------------------------------------
/src/ios/CDVWKWebViewEngine.h:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed to the Apache Software Foundation (ASF) under one
3 | or more contributor license agreements. See the NOTICE file
4 | distributed with this work for additional information
5 | regarding copyright ownership. The ASF licenses this file
6 | to you under the Apache License, Version 2.0 (the
7 | "License"); you may not use this file except in compliance
8 | with the License. You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing,
13 | software distributed under the License is distributed on an
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | KIND, either express or implied. See the License for the
16 | specific language governing permissions and limitations
17 | under the License.
18 | */
19 |
20 | #import
21 | #import
22 |
23 | @interface CDVWKWebViewEngine : CDVPlugin
24 |
25 | @property (nonatomic, strong, readonly) id uiDelegate;
26 | @property (nonatomic, strong) NSString * basePath;
27 |
28 | -(void)setServerBasePath:(CDVInvokedUrlCommand*)command;
29 | -(void)getServerBasePath:(CDVInvokedUrlCommand*)command;
30 |
31 | @end
32 |
--------------------------------------------------------------------------------
/src/ios/CDVWKWebViewEngine.m:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed to the Apache Software Foundation (ASF) under one
3 | or more contributor license agreements. See the NOTICE file
4 | distributed with this work for additional information
5 | regarding copyright ownership. The ASF licenses this file
6 | to you under the Apache License, Version 2.0 (the
7 | "License"); you may not use this file except in compliance
8 | with the License. You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing,
13 | software distributed under the License is distributed on an
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | KIND, either express or implied. See the License for the
16 | specific language governing permissions and limitations
17 | under the License.
18 | */
19 |
20 | #import
21 | #import
22 | #import
23 | #import
24 | #import
25 |
26 | #import "CDVWKWebViewEngine.h"
27 | #import "CDVWKWebViewUIDelegate.h"
28 | #import "CDVWKProcessPoolFactory.h"
29 | #import "IONAssetHandler.h"
30 |
31 | #define CDV_BRIDGE_NAME @"cordova"
32 | #define CDV_IONIC_STOP_SCROLL @"stopScroll"
33 | #define CDV_SERVER_PATH @"serverBasePath"
34 | #define LAST_BINARY_VERSION_CODE @"lastBinaryVersionCode"
35 | #define LAST_BINARY_VERSION_NAME @"lastBinaryVersionName"
36 |
37 | @implementation UIScrollView (BugIOS11)
38 |
39 | + (void)load {
40 | static dispatch_once_t onceToken;
41 | dispatch_once(&onceToken, ^{
42 | Class class = [self class];
43 | SEL originalSelector = @selector(init);
44 | SEL swizzledSelector = @selector(xxx_init);
45 |
46 | Method originalMethod = class_getInstanceMethod(class, originalSelector);
47 | Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
48 |
49 | BOOL didAddMethod =
50 | class_addMethod(class,
51 | originalSelector,
52 | method_getImplementation(swizzledMethod),
53 | method_getTypeEncoding(swizzledMethod));
54 |
55 | if (didAddMethod) {
56 | class_replaceMethod(class,
57 | swizzledSelector,
58 | method_getImplementation(originalMethod),
59 | method_getTypeEncoding(originalMethod));
60 | } else {
61 | method_exchangeImplementations(originalMethod, swizzledMethod);
62 | }
63 | });
64 | }
65 |
66 | #pragma mark - Method Swizzling
67 |
68 | - (id)xxx_init {
69 | id a = [self xxx_init];
70 | NSArray *stack = [NSThread callStackSymbols];
71 | for(NSString *trace in stack) {
72 | if([trace containsString:@"WebKit"]) {
73 | [a setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
74 | break;
75 | }
76 | }
77 | return a;
78 | }
79 |
80 | @end
81 |
82 |
83 | @interface CDVWKWeakScriptMessageHandler : NSObject
84 |
85 | @property (nonatomic, weak, readonly) idscriptMessageHandler;
86 |
87 | - (instancetype)initWithScriptMessageHandler:(id)scriptMessageHandler;
88 |
89 | @end
90 |
91 |
92 | @interface CDVWKWebViewEngine ()
93 |
94 | @property (nonatomic, strong, readwrite) UIView* engineWebView;
95 | @property (nonatomic, strong, readwrite) id uiDelegate;
96 | @property (nonatomic, weak) id weakScriptMessageHandler;
97 | @property (nonatomic, readwrite) CGRect frame;
98 | @property (nonatomic, strong) NSString *userAgentCreds;
99 | @property (nonatomic, strong) IONAssetHandler * handler;
100 |
101 | @property (nonatomic, readwrite) NSString *CDV_LOCAL_SERVER;
102 | @end
103 |
104 | // expose private configuration value required for background operation
105 | @interface WKWebViewConfiguration ()
106 |
107 | @end
108 |
109 |
110 | // see forwardingTargetForSelector: selector comment for the reason for this pragma
111 | #pragma clang diagnostic ignored "-Wprotocol"
112 |
113 | @implementation CDVWKWebViewEngine
114 |
115 | @synthesize engineWebView = _engineWebView;
116 |
117 | - (instancetype)initWithFrame:(CGRect)frame
118 | {
119 | self = [super init];
120 | if (self) {
121 | if (NSClassFromString(@"WKWebView") == nil) {
122 | return nil;
123 | }
124 | // add to keyWindow to ensure it is 'active'
125 | [UIApplication.sharedApplication.keyWindow addSubview:self.engineWebView];
126 |
127 | self.frame = frame;
128 | }
129 | return self;
130 | }
131 |
132 | -(NSString *) getStartPath {
133 | NSString * wwwPath = [[NSBundle mainBundle] pathForResource:@"www" ofType: nil];
134 |
135 | NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
136 | NSString * persistedPath = [userDefaults objectForKey:CDV_SERVER_PATH];
137 | if (![self isDeployDisabled] && ![self isNewBinary] && persistedPath && ![persistedPath isEqualToString:@""]) {
138 | NSString *libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
139 | NSString * cordovaDataDirectory = [libPath stringByAppendingPathComponent:@"NoCloud"];
140 | NSString * snapshots = [cordovaDataDirectory stringByAppendingPathComponent:@"ionic_built_snapshots"];
141 | wwwPath = [snapshots stringByAppendingPathComponent:[persistedPath lastPathComponent]];
142 | }
143 | self.basePath = wwwPath;
144 | return wwwPath;
145 | }
146 |
147 | -(BOOL) isNewBinary
148 | {
149 | NSString * versionCode = [[NSBundle mainBundle] infoDictionary][@"CFBundleVersion"];
150 | NSString * versionName = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
151 | NSUserDefaults * prefs = [NSUserDefaults standardUserDefaults];
152 | NSString * lastVersionCode = [prefs stringForKey:LAST_BINARY_VERSION_CODE];
153 | NSString * lastVersionName = [prefs stringForKey:LAST_BINARY_VERSION_NAME];
154 | if (![versionCode isEqualToString:lastVersionCode] || ![versionName isEqualToString:lastVersionName]) {
155 | [prefs setObject:versionCode forKey:LAST_BINARY_VERSION_CODE];
156 | [prefs setObject:versionName forKey:LAST_BINARY_VERSION_NAME];
157 | [prefs setObject:@"" forKey:CDV_SERVER_PATH];
158 | [prefs synchronize];
159 | return YES;
160 | }
161 | return NO;
162 | }
163 |
164 | -(BOOL) isDeployDisabled {
165 | return [[self.commandDelegate.settings objectForKey:[@"DisableDeploy" lowercaseString]] boolValue];
166 | }
167 |
168 | - (WKWebViewConfiguration*) createConfigurationFromSettings:(NSDictionary*)settings
169 | {
170 | WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
171 | configuration.processPool = [[CDVWKProcessPoolFactory sharedFactory] sharedProcessPool];
172 | configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
173 |
174 | if (settings == nil) {
175 | return configuration;
176 | }
177 |
178 | if(![settings cordovaBoolSettingForKey:@"WKSuspendInBackground" defaultValue:YES]){
179 | NSString* _BGStatus;
180 | if (@available(iOS 12.2, *)) {
181 | // do stuff for iOS 12.2 and newer
182 | NSLog(@"iOS 12.2+ detected");
183 | NSString* str = @"YWx3YXlzUnVuc0F0Rm9yZWdyb3VuZFByaW9yaXR5";
184 | NSData* data = [[NSData alloc] initWithBase64EncodedString:str options:0];
185 | _BGStatus = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
186 | } else {
187 | // do stuff for iOS 12.1 and older
188 | NSLog(@"iOS Below 12.2 detected");
189 | NSString* str = @"X2Fsd2F5c1J1bnNBdEZvcmVncm91bmRQcmlvcml0eQ==";
190 | NSData* data = [[NSData alloc] initWithBase64EncodedString:str options:0];
191 | _BGStatus = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
192 | }
193 | [configuration setValue:[NSNumber numberWithBool:YES]
194 | forKey:_BGStatus];
195 | }
196 | NSString *userAgent = configuration.applicationNameForUserAgent;
197 | if (
198 | [settings cordovaSettingForKey:@"OverrideUserAgent"] == nil &&
199 | [settings cordovaSettingForKey:@"AppendUserAgent"] != nil
200 | ) {
201 | userAgent = [NSString stringWithFormat:@"%@ %@", userAgent, [settings cordovaSettingForKey:@"AppendUserAgent"]];
202 | }
203 | configuration.applicationNameForUserAgent = userAgent;
204 | configuration.allowsInlineMediaPlayback = [settings cordovaBoolSettingForKey:@"AllowInlineMediaPlayback" defaultValue:YES];
205 | configuration.suppressesIncrementalRendering = [settings cordovaBoolSettingForKey:@"SuppressesIncrementalRendering" defaultValue:NO];
206 | configuration.allowsAirPlayForMediaPlayback = [settings cordovaBoolSettingForKey:@"MediaPlaybackAllowsAirPlay" defaultValue:YES];
207 | return configuration;
208 | }
209 |
210 | - (void)pluginInitialize
211 | {
212 | // viewController would be available now. we attempt to set all possible delegates to it, by default
213 | NSDictionary* settings = self.commandDelegate.settings;
214 | NSString *bind = [settings cordovaSettingForKey:@"Hostname"];
215 | if(bind == nil){
216 | bind = @"localhost";
217 | }
218 | NSString *scheme = [settings cordovaSettingForKey:@"iosScheme"];
219 | if(scheme == nil || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"] || [scheme isEqualToString:@"file"]){
220 | scheme = @"ionic";
221 | }
222 | self.CDV_LOCAL_SERVER = [NSString stringWithFormat:@"%@://%@", scheme, bind];
223 |
224 | self.uiDelegate = [[CDVWKWebViewUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]];
225 |
226 | CDVWKWeakScriptMessageHandler *weakScriptMessageHandler = [[CDVWKWeakScriptMessageHandler alloc] initWithScriptMessageHandler:self];
227 |
228 | WKUserContentController* userContentController = [[WKUserContentController alloc] init];
229 | [userContentController addScriptMessageHandler:weakScriptMessageHandler name:CDV_BRIDGE_NAME];
230 | [userContentController addScriptMessageHandler:weakScriptMessageHandler name:CDV_IONIC_STOP_SCROLL];
231 |
232 | // Inject XHR Polyfill
233 | NSLog(@"CDVWKWebViewEngine: trying to inject XHR polyfill");
234 | WKUserScript *wkScript = [self wkPluginScript];
235 | if (wkScript) {
236 | [userContentController addUserScript:wkScript];
237 | }
238 |
239 | WKUserScript *configScript = [self configScript];
240 | if (configScript) {
241 | [userContentController addUserScript:configScript];
242 | }
243 |
244 | BOOL autoCordova = [settings cordovaBoolSettingForKey:@"AutoInjectCordova" defaultValue:NO];
245 | if (autoCordova){
246 | NSLog(@"CDVWKWebViewEngine: trying to inject XHR polyfill");
247 | WKUserScript *cordova = [self autoCordovify];
248 | if (cordova) {
249 | [userContentController addUserScript:cordova];
250 | }
251 | }
252 |
253 | BOOL audioCanMix = [settings cordovaBoolSettingForKey:@"AudioCanMix" defaultValue:NO];
254 | if (audioCanMix) {
255 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
256 | withOptions:AVAudioSessionCategoryOptionMixWithOthers
257 | error:nil];
258 | }
259 |
260 | WKWebViewConfiguration* configuration = [self createConfigurationFromSettings:settings];
261 | configuration.userContentController = userContentController;
262 |
263 | self.handler = [[IONAssetHandler alloc] initWithBasePath:[self getStartPath] andScheme:scheme];
264 | [configuration setURLSchemeHandler:self.handler forURLScheme:scheme];
265 |
266 | // re-create WKWebView, since we need to update configuration
267 | // remove from keyWindow before recreating
268 | [self.engineWebView removeFromSuperview];
269 | WKWebView* wkWebView = [[WKWebView alloc] initWithFrame:self.frame configuration:configuration];
270 |
271 | [wkWebView.scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
272 |
273 | wkWebView.UIDelegate = self.uiDelegate;
274 | self.engineWebView = wkWebView;
275 | // add to keyWindow to ensure it is 'active'
276 | [UIApplication.sharedApplication.keyWindow addSubview:self.engineWebView];
277 |
278 | NSString * overrideUserAgent = [settings cordovaSettingForKey:@"OverrideUserAgent"];
279 | if (overrideUserAgent != nil) {
280 | wkWebView.customUserAgent = overrideUserAgent;
281 | }
282 |
283 | if ([self.viewController conformsToProtocol:@protocol(WKUIDelegate)]) {
284 | wkWebView.UIDelegate = (id )self.viewController;
285 | }
286 |
287 | if ([self.viewController conformsToProtocol:@protocol(WKNavigationDelegate)]) {
288 | wkWebView.navigationDelegate = (id )self.viewController;
289 | } else {
290 | wkWebView.navigationDelegate = (id )self;
291 | }
292 |
293 | if ([self.viewController conformsToProtocol:@protocol(WKScriptMessageHandler)]) {
294 | [wkWebView.configuration.userContentController addScriptMessageHandler:(id < WKScriptMessageHandler >)self.viewController name:CDV_BRIDGE_NAME];
295 | }
296 |
297 | [self keyboardDisplayDoesNotRequireUserAction];
298 |
299 | if ([settings cordovaBoolSettingForKey:@"KeyboardAppearanceDark" defaultValue:NO]) {
300 | [self setKeyboardAppearanceDark];
301 | }
302 |
303 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160400
304 | // With the introduction of iOS 16.4 the webview is no longer inspectable by default.
305 | // We'll honor that change for release builds, but will still allow inspection on debug builds by default.
306 | // We also introduce an override option, so consumers can influence this decision in their own build.
307 | if (@available(iOS 16.4, *)) {
308 | #ifdef DEBUG
309 | BOOL allowWebviewInspectionDefault = YES;
310 | #else
311 | BOOL allowWebviewInspectionDefault = NO;
312 | #endif
313 | wkWebView.inspectable = [settings cordovaBoolSettingForKey:@"InspectableWebview" defaultValue:allowWebviewInspectionDefault];
314 | }
315 | #endif
316 | [self updateSettings:settings];
317 |
318 | // check if content thread has died on resume
319 | NSLog(@"%@", @"CDVWKWebViewEngine will reload WKWebView if required on resume");
320 | [[NSNotificationCenter defaultCenter]
321 | addObserver:self
322 | selector:@selector(onAppWillEnterForeground:)
323 | name:UIApplicationWillEnterForegroundNotification object:nil];
324 |
325 | // If less than ios 13.4
326 | if (@available(iOS 13.4, *)) {} else {
327 | // 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)
328 | // Apple has released a fix in 13.4, but not in 12.x (as of 12.4.6)
329 | [[NSNotificationCenter defaultCenter]
330 | addObserver:self
331 | selector:@selector(keyboardWillHide)
332 | name:UIKeyboardWillHideNotification object:nil];
333 | }
334 |
335 | NSLog(@"Using Ionic WKWebView");
336 |
337 | }
338 |
339 | // https://github.com/Telerik-Verified-Plugins/WKWebView/commit/04e8296adeb61f289f9c698045c19b62d080c7e3#L609-L620
340 | - (void) keyboardDisplayDoesNotRequireUserAction {
341 | Class class = NSClassFromString(@"WKContentView");
342 | NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0};
343 | NSOperatingSystemVersion iOS_12_2_0 = (NSOperatingSystemVersion){12, 2, 0};
344 | NSOperatingSystemVersion iOS_13_0_0 = (NSOperatingSystemVersion){13, 0, 0};
345 | char * methodSignature = "_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:";
346 |
347 | if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_13_0_0]) {
348 | methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:";
349 | } else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_12_2_0]) {
350 | methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:";
351 | }
352 |
353 | if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) {
354 | SEL selector = sel_getUid(methodSignature);
355 | Method method = class_getInstanceMethod(class, selector);
356 | IMP original = method_getImplementation(method);
357 | IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
358 | ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
359 | });
360 | method_setImplementation(method, override);
361 | } else {
362 | SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:");
363 | Method method = class_getInstanceMethod(class, selector);
364 | IMP original = method_getImplementation(method);
365 | IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) {
366 | ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3);
367 | });
368 | method_setImplementation(method, override);
369 | }
370 | }
371 |
372 | - (void)setKeyboardAppearanceDark
373 | {
374 | IMP darkImp = imp_implementationWithBlock(^(id _s) {
375 | return UIKeyboardAppearanceDark;
376 | });
377 | for (NSString* classString in @[@"WKContentView", @"UITextInputTraits"]) {
378 | Class c = NSClassFromString(classString);
379 | Method m = class_getInstanceMethod(c, @selector(keyboardAppearance));
380 | if (m != NULL) {
381 | method_setImplementation(m, darkImp);
382 | } else {
383 | class_addMethod(c, @selector(keyboardAppearance), darkImp, "l@:");
384 | }
385 | }
386 | }
387 |
388 |
389 |
390 | - (void)onAppWillEnterForeground:(NSNotification *)notification {
391 | if ([self shouldReloadWebView]) {
392 | NSLog(@"%@", @"CDVWKWebViewEngine reloading!");
393 | [(WKWebView*)_engineWebView reload];
394 | }
395 | }
396 |
397 |
398 | -(void)keyboardWillHide
399 | {
400 | // 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)
401 | UIScrollView * scrollView = self.webView.scrollView;
402 | // Calculate some vars for convenience
403 | CGFloat contentLengthWithInsets = scrollView.contentSize.height + scrollView.adjustedContentInset.top + scrollView.adjustedContentInset.bottom;
404 | CGFloat contentOffsetY = scrollView.contentOffset.y;
405 | CGFloat screenHeight = scrollView.frame.size.height;
406 | CGFloat maxAllowedOffsetY = fmax(contentLengthWithInsets - screenHeight, 0); // 0 is for the case where content is shorter than screen
407 |
408 | // If the keyboard allowed the user to get to an offset beyond the max
409 | if (contentOffsetY > maxAllowedOffsetY) {
410 | // Reset the scroll to the max allowed so that there is no additional empty white space at the bottom where the keyboard occupied!
411 | CGPoint bottomOfPage = CGPointMake(scrollView.contentOffset.x, maxAllowedOffsetY);
412 | [scrollView setContentOffset:bottomOfPage];
413 | }
414 | }
415 |
416 | - (BOOL)shouldReloadWebView
417 | {
418 | WKWebView* wkWebView = (WKWebView*)_engineWebView;
419 | return [self shouldReloadWebView:wkWebView.URL title:wkWebView.title];
420 | }
421 |
422 | - (BOOL)shouldReloadWebView:(NSURL *)location title:(NSString*)title
423 | {
424 | BOOL title_is_nil = (title == nil);
425 | BOOL location_is_blank = [[location absoluteString] isEqualToString:@"about:blank"];
426 |
427 | BOOL reload = (title_is_nil || location_is_blank);
428 |
429 | #ifdef DEBUG
430 | NSLog(@"%@", @"CDVWKWebViewEngine shouldReloadWebView::");
431 | NSLog(@"CDVWKWebViewEngine shouldReloadWebView title: %@", title);
432 | NSLog(@"CDVWKWebViewEngine shouldReloadWebView location: %@", [location absoluteString]);
433 | NSLog(@"CDVWKWebViewEngine shouldReloadWebView reload: %u", reload);
434 | #endif
435 |
436 | return reload;
437 | }
438 |
439 |
440 | - (id)loadRequest:(NSURLRequest *)request
441 | {
442 | if (request.URL.fileURL) {
443 | NSURL* startURL = [NSURL URLWithString:((CDVViewController *)self.viewController).startPage];
444 | NSString* startFilePath = [self.commandDelegate pathForResource:[startURL path]];
445 | NSURL *url = [[NSURL URLWithString:self.CDV_LOCAL_SERVER] URLByAppendingPathComponent:request.URL.path];
446 | if ([request.URL.path isEqualToString:startFilePath]) {
447 | url = [NSURL URLWithString:self.CDV_LOCAL_SERVER];
448 | }
449 | if(request.URL.query) {
450 | url = [NSURL URLWithString:[@"?" stringByAppendingString:request.URL.query] relativeToURL:url];
451 | }
452 | if(request.URL.fragment) {
453 | url = [NSURL URLWithString:[@"#" stringByAppendingString:request.URL.fragment] relativeToURL:url];
454 | }
455 | request = [NSURLRequest requestWithURL:url];
456 | }
457 | return [(WKWebView*)_engineWebView loadRequest:request];
458 | }
459 |
460 | - (id)loadHTMLString:(NSString *)string baseURL:(NSURL*)baseURL
461 | {
462 | return [(WKWebView*)_engineWebView loadHTMLString:string baseURL:baseURL];
463 | }
464 |
465 | - (NSURL*) URL
466 | {
467 | return [(WKWebView*)_engineWebView URL];
468 | }
469 |
470 | - (BOOL)canLoadRequest:(NSURLRequest *)request
471 | {
472 | return TRUE;
473 | }
474 |
475 | - (void)updateSettings:(NSDictionary *)settings
476 | {
477 | WKWebView* wkWebView = (WKWebView *)_engineWebView;
478 |
479 | // By default, DisallowOverscroll is false (thus bounce is allowed)
480 | BOOL bounceAllowed = !([settings cordovaBoolSettingForKey:@"DisallowOverscroll" defaultValue:NO]);
481 |
482 | // prevent webView from bouncing
483 | if (!bounceAllowed) {
484 | if ([wkWebView respondsToSelector:@selector(scrollView)]) {
485 | ((UIScrollView*)[wkWebView scrollView]).bounces = NO;
486 | } else {
487 | for (id subview in wkWebView.subviews) {
488 | if ([[subview class] isSubclassOfClass:[UIScrollView class]]) {
489 | ((UIScrollView*)subview).bounces = NO;
490 | }
491 | }
492 | }
493 | }
494 |
495 | wkWebView.configuration.preferences.minimumFontSize = [settings cordovaFloatSettingForKey:@"MinimumFontSize" defaultValue:0.0];
496 | wkWebView.allowsLinkPreview = [settings cordovaBoolSettingForKey:@"AllowLinkPreview" defaultValue:NO];
497 | wkWebView.scrollView.scrollEnabled = [settings cordovaBoolSettingForKey:@"ScrollEnabled" defaultValue:NO];
498 | wkWebView.allowsBackForwardNavigationGestures = [settings cordovaBoolSettingForKey:@"AllowBackForwardNavigationGestures" defaultValue:NO];
499 | }
500 |
501 | - (void)updateWithInfo:(NSDictionary *)info
502 | {
503 | NSDictionary* scriptMessageHandlers = [info objectForKey:kCDVWebViewEngineScriptMessageHandlers];
504 | NSDictionary* settings = [info objectForKey:kCDVWebViewEngineWebViewPreferences];
505 | id navigationDelegate = [info objectForKey:kCDVWebViewEngineWKNavigationDelegate];
506 | id uiDelegate = [info objectForKey:kCDVWebViewEngineWKUIDelegate];
507 |
508 | WKWebView* wkWebView = (WKWebView*)_engineWebView;
509 |
510 | if (scriptMessageHandlers && [scriptMessageHandlers isKindOfClass:[NSDictionary class]]) {
511 | NSArray* allKeys = [scriptMessageHandlers allKeys];
512 |
513 | for (NSString* key in allKeys) {
514 | id object = [scriptMessageHandlers objectForKey:key];
515 | if ([object conformsToProtocol:@protocol(WKScriptMessageHandler)]) {
516 | [wkWebView.configuration.userContentController addScriptMessageHandler:object name:key];
517 | }
518 | }
519 | }
520 |
521 | if (navigationDelegate && [navigationDelegate conformsToProtocol:@protocol(WKNavigationDelegate)]) {
522 | wkWebView.navigationDelegate = navigationDelegate;
523 | }
524 |
525 | if (uiDelegate && [uiDelegate conformsToProtocol:@protocol(WKUIDelegate)]) {
526 | wkWebView.UIDelegate = uiDelegate;
527 | }
528 |
529 | if (settings && [settings isKindOfClass:[NSDictionary class]]) {
530 | [self updateSettings:settings];
531 | }
532 | }
533 |
534 | // This forwards the methods that are in the header that are not implemented here.
535 | // loadHTMLString:baseURL:
536 | // loadRequest:
537 | - (id)forwardingTargetForSelector:(SEL)aSelector
538 | {
539 | return _engineWebView;
540 | }
541 |
542 | - (UIView *)webView
543 | {
544 | return self.engineWebView;
545 | }
546 |
547 | - (WKUserScript *)wkPluginScript
548 | {
549 | NSString *scriptFile = [[NSBundle mainBundle] pathForResource:@"www/wk-plugin" ofType:@"js"];
550 | if (scriptFile == nil) {
551 | NSLog(@"CDVWKWebViewEngine: WK plugin was not found");
552 | return nil;
553 | }
554 | NSError *error = nil;
555 | NSString *source = [NSString stringWithContentsOfFile:scriptFile encoding:NSUTF8StringEncoding error:&error];
556 | if (source == nil || error != nil) {
557 | NSLog(@"CDVWKWebViewEngine: WK plugin can not be loaded: %@", error);
558 | return nil;
559 | }
560 | source = [source stringByAppendingString:[NSString stringWithFormat:@"window.WEBVIEW_SERVER_URL = '%@';", self.CDV_LOCAL_SERVER]];
561 |
562 | return [[WKUserScript alloc] initWithSource:source
563 | injectionTime:WKUserScriptInjectionTimeAtDocumentStart
564 | forMainFrameOnly:YES];
565 | }
566 |
567 | - (WKUserScript *)configScript
568 | {
569 | Class keyboard = NSClassFromString(@"CDVIonicKeyboard");
570 | BOOL keyboardPlugin = keyboard != nil;
571 | if(!keyboardPlugin) {
572 | return nil;
573 | }
574 |
575 | BOOL keyboardResizes = [self.commandDelegate.settings cordovaBoolSettingForKey:@"KeyboardResize" defaultValue:YES];
576 | NSString *source = [NSString stringWithFormat:
577 | @"window.Ionic = window.Ionic || {};"
578 | @"window.Ionic.keyboardPlugin=true;"
579 | @"window.Ionic.keyboardResizes=%@",
580 | keyboardResizes ? @"true" : @"false"];
581 |
582 | return [[WKUserScript alloc] initWithSource:source
583 | injectionTime:WKUserScriptInjectionTimeAtDocumentStart
584 | forMainFrameOnly:YES];
585 | }
586 |
587 | - (WKUserScript *)autoCordovify
588 | {
589 | NSURL *cordovaURL = [[NSBundle mainBundle] URLForResource:@"www/cordova" withExtension:@"js"];
590 | if (cordovaURL == nil) {
591 | NSLog(@"CDVWKWebViewEngine: cordova.js WAS NOT FOUND");
592 | return nil;
593 | }
594 | NSError *error = nil;
595 | NSString *source = [NSString stringWithContentsOfURL:cordovaURL encoding:NSUTF8StringEncoding error:&error];
596 | if (source == nil || error != nil) {
597 | NSLog(@"CDVWKWebViewEngine: cordova.js can not be loaded: %@", error);
598 | return nil;
599 | }
600 | NSLog(@"CDVWKWebViewEngine: auto injecting cordova");
601 | NSString *cordovaPath = [self.CDV_LOCAL_SERVER stringByAppendingString:cordovaURL.URLByDeletingLastPathComponent.path];
602 | NSString *replacement = [NSString stringWithFormat:@"var pathPrefix = '%@/';", cordovaPath];
603 | source = [source stringByReplacingOccurrencesOfString:@"var pathPrefix = findCordovaPath();" withString:replacement];
604 |
605 | return [[WKUserScript alloc] initWithSource:source
606 | injectionTime:WKUserScriptInjectionTimeAtDocumentStart
607 | forMainFrameOnly:YES];
608 | }
609 |
610 | #pragma mark WKScriptMessageHandler implementation
611 |
612 | - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
613 | {
614 | if ([message.name isEqualToString:CDV_BRIDGE_NAME]) {
615 | [self handleCordovaMessage: message];
616 | } else if ([message.name isEqualToString:CDV_IONIC_STOP_SCROLL]) {
617 | [self handleStopScroll];
618 | }
619 | }
620 |
621 | - (void)handleCordovaMessage:(WKScriptMessage*)message
622 | {
623 | CDVViewController *vc = (CDVViewController*)self.viewController;
624 |
625 | NSArray *jsonEntry = message.body; // NSString:callbackId, NSString:service, NSString:action, NSArray:args
626 | CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry];
627 | CDV_EXEC_LOG(@"Exec(%@): Calling %@.%@", command.callbackId, command.className, command.methodName);
628 |
629 | if (![vc.commandQueue execute:command]) {
630 | #ifdef DEBUG
631 | NSError* error = nil;
632 | NSString* commandJson = nil;
633 | NSData* jsonData = [NSJSONSerialization dataWithJSONObject:jsonEntry
634 | options:0
635 | error:&error];
636 |
637 | if (error == nil) {
638 | commandJson = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
639 | }
640 |
641 | static NSUInteger maxLogLength = 1024;
642 | NSString* commandString = ([commandJson length] > maxLogLength) ?
643 | [NSString stringWithFormat : @"%@[...]", [commandJson substringToIndex:maxLogLength]] :
644 | commandJson;
645 |
646 | NSLog(@"FAILED pluginJSON = %@", commandString);
647 | #endif
648 | }
649 | }
650 |
651 | - (void)handleStopScroll
652 | {
653 | WKWebView* wkWebView = (WKWebView*)_engineWebView;
654 | NSLog(@"CDVWKWebViewEngine: handleStopScroll");
655 | [self recursiveStopScroll:[wkWebView scrollView]];
656 | [wkWebView evaluateJavaScript:@"window.IonicStopScroll.fire()" completionHandler:nil];
657 | }
658 |
659 | - (void)recursiveStopScroll:(UIView *)node
660 | {
661 | if([node isKindOfClass: [UIScrollView class]]) {
662 | UIScrollView *nodeAsScroll = (UIScrollView *)node;
663 |
664 | if([nodeAsScroll isScrollEnabled] && ![nodeAsScroll isHidden]) {
665 | [nodeAsScroll setScrollEnabled: NO];
666 | [nodeAsScroll setScrollEnabled: YES];
667 | }
668 | }
669 |
670 | // iterate tree recursivelly
671 | for (UIView *child in [node subviews]) {
672 | [self recursiveStopScroll:child];
673 | }
674 | }
675 |
676 |
677 | #pragma mark WKNavigationDelegate implementation
678 |
679 | - (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(WKNavigation*)navigation
680 | {
681 | [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginResetNotification object:webView]];
682 | }
683 |
684 | - (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation
685 | {
686 | #ifndef __CORDOVA_6_0_0
687 | CDVViewController* vc = (CDVViewController*)self.viewController;
688 | [CDVUserAgentUtil releaseLock:vc.userAgentLockToken];
689 | #endif
690 | [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPageDidLoadNotification object:webView]];
691 | }
692 |
693 | - (void)webView:(WKWebView*)theWebView didFailProvisionalNavigation:(WKNavigation*)navigation withError:(NSError*)error
694 | {
695 | [self webView:theWebView didFailNavigation:navigation withError:error];
696 | }
697 |
698 | - (void)webView:(WKWebView*)theWebView didFailNavigation:(WKNavigation*)navigation withError:(NSError*)error
699 | {
700 | CDVViewController* vc = (CDVViewController*)self.viewController;
701 | #ifndef __CORDOVA_6_0_0
702 | [CDVUserAgentUtil releaseLock:vc.userAgentLockToken];
703 | #endif
704 |
705 | NSString* message = [NSString stringWithFormat:@"Failed to load webpage with error: %@", [error localizedDescription]];
706 | NSLog(@"%@", message);
707 |
708 | NSURL* errorUrl = vc.errorURL;
709 | if (errorUrl) {
710 | NSCharacterSet *charSet = [NSCharacterSet URLFragmentAllowedCharacterSet];
711 | errorUrl = [NSURL URLWithString:[NSString stringWithFormat:@"?error=%@", [message stringByAddingPercentEncodingWithAllowedCharacters:charSet]] relativeToURL:errorUrl];
712 | NSLog(@"%@", [errorUrl absoluteString]);
713 | [theWebView loadRequest:[NSURLRequest requestWithURL:errorUrl]];
714 | }
715 | #ifdef DEBUG
716 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] message:message preferredStyle:UIAlertControllerStyleAlert];
717 | [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:nil]];
718 | [vc presentViewController:alertController animated:YES completion:nil];
719 | #endif
720 | }
721 |
722 | - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView
723 | {
724 | [webView reload];
725 | }
726 |
727 | - (BOOL)defaultResourcePolicyForURL:(NSURL*)url
728 | {
729 | // all file:// urls are allowed
730 | if ([url isFileURL]) {
731 | return YES;
732 | }
733 |
734 | return NO;
735 | }
736 |
737 | - (void) webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
738 | {
739 | NSURL* url = [navigationAction.request URL];
740 | CDVViewController* vc = (CDVViewController*)self.viewController;
741 |
742 | /*
743 | * Give plugins the chance to handle the url
744 | */
745 | BOOL anyPluginsResponded = NO;
746 | BOOL shouldAllowRequest = NO;
747 |
748 | for (NSString* pluginName in vc.pluginObjects) {
749 | CDVPlugin* plugin = [vc.pluginObjects objectForKey:pluginName];
750 | SEL selector = NSSelectorFromString(@"shouldOverrideLoadWithRequest:navigationType:");
751 | if ([plugin respondsToSelector:selector]) {
752 | anyPluginsResponded = YES;
753 | // https://issues.apache.org/jira/browse/CB-12497
754 | int navType = (int)navigationAction.navigationType;
755 | if (WKNavigationTypeOther == navigationAction.navigationType) {
756 | #ifdef __CORDOVA_6_0_0
757 | navType = -1;
758 | #else
759 | navType = 5;
760 | #endif
761 | }
762 | shouldAllowRequest = (((BOOL (*)(id, SEL, id, int))objc_msgSend)(plugin, selector, navigationAction.request, navType));
763 | if (!shouldAllowRequest) {
764 | break;
765 | }
766 | }
767 | }
768 |
769 | if (!anyPluginsResponded) {
770 | /*
771 | * Handle all other types of urls (tel:, sms:), and requests to load a url in the main webview.
772 | */
773 | shouldAllowRequest = [self defaultResourcePolicyForURL:url];
774 | if (!shouldAllowRequest) {
775 | [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]];
776 | }
777 | }
778 |
779 | if (shouldAllowRequest) {
780 | NSString *scheme = url.scheme;
781 | if ([scheme isEqualToString:@"tel"] ||
782 | [scheme isEqualToString:@"mailto"] ||
783 | [scheme isEqualToString:@"facetime"] ||
784 | [scheme isEqualToString:@"sms"] ||
785 | [scheme isEqualToString:@"maps"]) {
786 | [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
787 | decisionHandler(WKNavigationActionPolicyCancel);
788 | } else {
789 | decisionHandler(WKNavigationActionPolicyAllow);
790 | }
791 | } else {
792 | decisionHandler(WKNavigationActionPolicyCancel);
793 | }
794 | }
795 |
796 | -(void)getServerBasePath:(CDVInvokedUrlCommand*)command
797 | {
798 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:self.basePath] callbackId:command.callbackId];
799 | }
800 |
801 | -(void)setServerBasePath:(CDVInvokedUrlCommand*)command
802 | {
803 | NSString * path = [command argumentAtIndex:0];
804 | self.basePath = path;
805 | [self.handler setAssetPath:path];
806 |
807 | NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.CDV_LOCAL_SERVER]];
808 | [(WKWebView*)_engineWebView loadRequest:request];
809 | }
810 |
811 | -(void)persistServerBasePath:(CDVInvokedUrlCommand*)command
812 | {
813 | NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
814 | [userDefaults setObject:[self.basePath lastPathComponent] forKey:CDV_SERVER_PATH];
815 | [userDefaults synchronize];
816 | }
817 |
818 | @end
819 |
820 | #pragma mark - CDVWKWeakScriptMessageHandler
821 |
822 | @implementation CDVWKWeakScriptMessageHandler
823 |
824 | - (instancetype)initWithScriptMessageHandler:(id)scriptMessageHandler
825 | {
826 | self = [super init];
827 | if (self) {
828 | _scriptMessageHandler = scriptMessageHandler;
829 | }
830 | return self;
831 | }
832 |
833 | - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
834 | {
835 | [self.scriptMessageHandler userContentController:userContentController didReceiveScriptMessage:message];
836 | }
837 |
838 | @end
839 |
--------------------------------------------------------------------------------
/src/ios/CDVWKWebViewUIDelegate.h:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed to the Apache Software Foundation (ASF) under one
3 | or more contributor license agreements. See the NOTICE file
4 | distributed with this work for additional information
5 | regarding copyright ownership. The ASF licenses this file
6 | to you under the Apache License, Version 2.0 (the
7 | "License"); you may not use this file except in compliance
8 | with the License. You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing,
13 | software distributed under the License is distributed on an
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | KIND, either express or implied. See the License for the
16 | specific language governing permissions and limitations
17 | under the License.
18 | */
19 |
20 | #import
21 |
22 | @interface CDVWKWebViewUIDelegate : NSObject
23 |
24 | @property (nonatomic, copy) NSString* title;
25 |
26 | - (instancetype)initWithTitle:(NSString*)title;
27 |
28 | @end
29 |
--------------------------------------------------------------------------------
/src/ios/CDVWKWebViewUIDelegate.m:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed to the Apache Software Foundation (ASF) under one
3 | or more contributor license agreements. See the NOTICE file
4 | distributed with this work for additional information
5 | regarding copyright ownership. The ASF licenses this file
6 | to you under the Apache License, Version 2.0 (the
7 | "License"); you may not use this file except in compliance
8 | with the License. You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing,
13 | software distributed under the License is distributed on an
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | KIND, either express or implied. See the License for the
16 | specific language governing permissions and limitations
17 | under the License.
18 | */
19 |
20 | #import "CDVWKWebViewUIDelegate.h"
21 |
22 | @implementation CDVWKWebViewUIDelegate
23 |
24 | - (instancetype)initWithTitle:(NSString*)title
25 | {
26 | self = [super init];
27 | if (self) {
28 | self.title = title;
29 | }
30 |
31 | return self;
32 | }
33 |
34 | - (void) webView:(WKWebView*)webView runJavaScriptAlertPanelWithMessage:(NSString*)message
35 | initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(void))completionHandler
36 | {
37 | UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title
38 | message:message
39 | preferredStyle:UIAlertControllerStyleAlert];
40 |
41 | UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK")
42 | style:UIAlertActionStyleDefault
43 | handler:^(UIAlertAction* action)
44 | {
45 | completionHandler();
46 | [alert dismissViewControllerAnimated:YES completion:nil];
47 | }];
48 |
49 | [alert addAction:ok];
50 |
51 | UIViewController* rootController = [UIApplication sharedApplication].delegate.window.rootViewController;
52 |
53 | [rootController presentViewController:alert animated:YES completion:nil];
54 | }
55 |
56 | - (void) webView:(WKWebView*)webView runJavaScriptConfirmPanelWithMessage:(NSString*)message
57 | initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(BOOL result))completionHandler
58 | {
59 | UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title
60 | message:message
61 | preferredStyle:UIAlertControllerStyleAlert];
62 |
63 | UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK")
64 | style:UIAlertActionStyleDefault
65 | handler:^(UIAlertAction* action)
66 | {
67 | completionHandler(YES);
68 | [alert dismissViewControllerAnimated:YES completion:nil];
69 | }];
70 |
71 | [alert addAction:ok];
72 |
73 | UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel")
74 | style:UIAlertActionStyleDefault
75 | handler:^(UIAlertAction* action)
76 | {
77 | completionHandler(NO);
78 | [alert dismissViewControllerAnimated:YES completion:nil];
79 | }];
80 | [alert addAction:cancel];
81 |
82 | UIViewController* rootController = [UIApplication sharedApplication].delegate.window.rootViewController;
83 |
84 | [rootController presentViewController:alert animated:YES completion:nil];
85 | }
86 |
87 | - (void) webView:(WKWebView*)webView runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt
88 | defaultText:(NSString*)defaultText initiatedByFrame:(WKFrameInfo*)frame
89 | completionHandler:(void (^)(NSString* result))completionHandler
90 | {
91 | UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title
92 | message:prompt
93 | preferredStyle:UIAlertControllerStyleAlert];
94 |
95 | UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK")
96 | style:UIAlertActionStyleDefault
97 | handler:^(UIAlertAction* action)
98 | {
99 | completionHandler(((UITextField*)alert.textFields[0]).text);
100 | [alert dismissViewControllerAnimated:YES completion:nil];
101 | }];
102 |
103 | [alert addAction:ok];
104 |
105 | UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel")
106 | style:UIAlertActionStyleDefault
107 | handler:^(UIAlertAction* action)
108 | {
109 | completionHandler(nil);
110 | [alert dismissViewControllerAnimated:YES completion:nil];
111 | }];
112 | [alert addAction:cancel];
113 |
114 | [alert addTextFieldWithConfigurationHandler:^(UITextField* textField) {
115 | textField.text = defaultText;
116 | }];
117 |
118 | UIViewController* rootController = [UIApplication sharedApplication].delegate.window.rootViewController;
119 |
120 | [rootController presentViewController:alert animated:YES completion:nil];
121 | }
122 |
123 | @end
124 |
--------------------------------------------------------------------------------
/src/ios/IONAssetHandler.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface IONAssetHandler : NSObject
5 |
6 | @property (nonatomic, strong) NSString * basePath;
7 | @property (nonatomic, strong) NSString * scheme;
8 |
9 | -(void)setAssetPath:(NSString *)assetPath;
10 | - (instancetype)initWithBasePath:(NSString *)basePath andScheme:(NSString *)scheme;
11 |
12 |
13 | @end
14 |
--------------------------------------------------------------------------------
/src/ios/IONAssetHandler.m:
--------------------------------------------------------------------------------
1 | #import "IONAssetHandler.h"
2 | #import
3 | #import "CDVWKWebViewEngine.h"
4 |
5 | @implementation IONAssetHandler
6 |
7 | -(void)setAssetPath:(NSString *)assetPath {
8 | self.basePath = assetPath;
9 | }
10 |
11 | - (instancetype)initWithBasePath:(NSString *)basePath andScheme:(NSString *)scheme {
12 | self = [super init];
13 | if (self) {
14 | _basePath = basePath;
15 | _scheme = scheme;
16 | }
17 | return self;
18 | }
19 |
20 | - (void)webView:(WKWebView *)webView startURLSchemeTask:(id )urlSchemeTask
21 | {
22 | NSString * startPath = @"";
23 | NSURL * url = urlSchemeTask.request.URL;
24 | NSString * stringToLoad = url.path;
25 | NSString * scheme = url.scheme;
26 |
27 | if ([scheme isEqualToString:self.scheme]) {
28 | if ([stringToLoad hasPrefix:@"/_app_file_"]) {
29 | startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""];
30 | } else {
31 | startPath = self.basePath ? self.basePath : @"";
32 | if ([stringToLoad isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) {
33 | startPath = [startPath stringByAppendingString:@"/index.html"];
34 | } else {
35 | startPath = [startPath stringByAppendingString:stringToLoad];
36 | }
37 | }
38 | }
39 | NSError * fileError = nil;
40 | NSData * data = nil;
41 | if ([self isMediaExtension:url.pathExtension]) {
42 | data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError];
43 | }
44 | if (!data || fileError) {
45 | data = [[NSData alloc] initWithContentsOfFile:startPath];
46 | }
47 | NSInteger statusCode = 200;
48 | if (!data) {
49 | statusCode = 404;
50 | }
51 | NSURL * localUrl = [NSURL URLWithString:url.absoluteString];
52 | NSString * mimeType = [self getMimeType:url.pathExtension];
53 | id response = nil;
54 | if (data && [self isMediaExtension:url.pathExtension]) {
55 | response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
56 | } else {
57 | NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"};
58 | response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers];
59 | }
60 |
61 | [urlSchemeTask didReceiveResponse:response];
62 | [urlSchemeTask didReceiveData:data];
63 | [urlSchemeTask didFinish];
64 |
65 | }
66 |
67 | - (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id)urlSchemeTask
68 | {
69 | NSLog(@"stop");
70 | }
71 |
72 | -(NSString *) getMimeType:(NSString *)fileExtension {
73 | if (fileExtension && ![fileExtension isEqualToString:@""]) {
74 | NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL);
75 | NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
76 | return contentType ? contentType : @"application/octet-stream";
77 | } else {
78 | return @"text/html";
79 | }
80 | }
81 |
82 | -(BOOL) isMediaExtension:(NSString *) pathExtension {
83 | NSArray * mediaExtensions = @[@"m4v", @"mov", @"mp4",
84 | @"aac", @"ac3", @"aiff", @"au", @"flac", @"m4a", @"mp3", @"wav"];
85 | if ([mediaExtensions containsObject:pathExtension.lowercaseString]) {
86 | return YES;
87 | }
88 | return NO;
89 | }
90 |
91 |
92 | @end
93 |
--------------------------------------------------------------------------------
/src/ios/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Niklas von Hertzen
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/src/ios/wk-plugin.js:
--------------------------------------------------------------------------------
1 |
2 | (function _wk_plugin() {
3 | // Check if we are running in WKWebView
4 | if (!window.webkit || !window.webkit.messageHandlers) {
5 | return;
6 | }
7 |
8 | // Initialize Ionic
9 | window.Ionic = window.Ionic || {};
10 |
11 | var stopScrollHandler = window.webkit.messageHandlers.stopScroll;
12 | if (!stopScrollHandler) {
13 | console.error('Can not find stopScroll handler');
14 | return;
15 | }
16 |
17 | var stopScrollFunc = null;
18 | var stopScroll = {
19 | stop: function stop(callback) {
20 | if (!stopScrollFunc) {
21 | stopScrollFunc = callback;
22 | stopScrollHandler.postMessage('');
23 | }
24 | },
25 | fire: function fire() {
26 | stopScrollFunc && stopScrollFunc();
27 | stopScrollFunc = null;
28 | },
29 | cancel: function cancel() {
30 | stopScrollFunc = null;
31 | }
32 | };
33 |
34 | window.Ionic.StopScroll = stopScroll;
35 | // deprecated
36 | window.IonicStopScroll = stopScroll;
37 |
38 | console.debug("Ionic Stop Scroll injected!");
39 | })();
40 |
--------------------------------------------------------------------------------
/src/www/ios/ios-wkwebview-exec.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * Licensed to the Apache Software Foundation (ASF) under one
4 | * or more contributor license agreements. See the NOTICE file
5 | * distributed with this work for additional information
6 | * regarding copyright ownership. The ASF licenses this file
7 | * to you under the Apache License, Version 2.0 (the
8 | * "License"); you may not use this file except in compliance
9 | * with the License. You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing,
14 | * software distributed under the License is distributed on an
15 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | * KIND, either express or implied. See the License for the
17 | * specific language governing permissions and limitations
18 | * under the License.
19 | *
20 | */
21 |
22 | /**
23 | * Creates the exec bridge used to notify the native code of
24 | * commands.
25 | */
26 | var cordova = require('cordova');
27 | var utils = require('cordova/utils');
28 | var base64 = require('cordova/base64');
29 |
30 | function massageArgsJsToNative (args) {
31 | if (!args || utils.typeName(args) !== 'Array') {
32 | return args;
33 | }
34 | var ret = [];
35 | args.forEach(function (arg, i) {
36 | if (utils.typeName(arg) === 'ArrayBuffer') {
37 | ret.push({
38 | 'CDVType': 'ArrayBuffer',
39 | 'data': base64.fromArrayBuffer(arg)
40 | });
41 | } else {
42 | ret.push(arg);
43 | }
44 | });
45 | return ret;
46 | }
47 |
48 | function massageMessageNativeToJs (message) {
49 | if (message.CDVType === 'ArrayBuffer') {
50 | var stringToArrayBuffer = function (str) {
51 | var ret = new Uint8Array(str.length);
52 | for (var i = 0; i < str.length; i++) {
53 | ret[i] = str.charCodeAt(i);
54 | }
55 | return ret.buffer;
56 | };
57 | var base64ToArrayBuffer = function (b64) {
58 | return stringToArrayBuffer(atob(b64)); // eslint-disable-line no-undef
59 | };
60 | message = base64ToArrayBuffer(message.data);
61 | }
62 | return message;
63 | }
64 |
65 | function convertMessageToArgsNativeToJs (message) {
66 | var args = [];
67 | if (!message || !message.hasOwnProperty('CDVType')) {
68 | args.push(message);
69 | } else if (message.CDVType === 'MultiPart') {
70 | message.messages.forEach(function (e) {
71 | args.push(massageMessageNativeToJs(e));
72 | });
73 | } else {
74 | args.push(massageMessageNativeToJs(message));
75 | }
76 | return args;
77 | }
78 |
79 | var iOSExec = function () {
80 | // detect change in bridge, if there is a change, we forward to new bridge
81 |
82 | // if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.cordova && window.webkit.messageHandlers.cordova.postMessage) {
83 | // bridgeMode = jsToNativeModes.WK_WEBVIEW_BINDING;
84 | // }
85 |
86 | var successCallback, failCallback, service, action, actionArgs;
87 | var callbackId = null;
88 | if (typeof arguments[0] !== 'string') {
89 | // FORMAT ONE
90 | successCallback = arguments[0];
91 | failCallback = arguments[1];
92 | service = arguments[2];
93 | action = arguments[3];
94 | actionArgs = arguments[4];
95 |
96 | // Since we need to maintain backwards compatibility, we have to pass
97 | // an invalid callbackId even if no callback was provided since plugins
98 | // will be expecting it. The Cordova.exec() implementation allocates
99 | // an invalid callbackId and passes it even if no callbacks were given.
100 | callbackId = 'INVALID';
101 | } else {
102 | throw new Error('The old format of this exec call has been removed (deprecated since 2.1). Change to: ' + // eslint-disable-line
103 | 'cordova.exec(null, null, \'Service\', \'action\', [ arg1, arg2 ]);');
104 | }
105 |
106 | // If actionArgs is not provided, default to an empty array
107 | actionArgs = actionArgs || [];
108 |
109 | // Register the callbacks and add the callbackId to the positional
110 | // arguments if given.
111 | if (successCallback || failCallback) {
112 | callbackId = service + cordova.callbackId++;
113 | cordova.callbacks[callbackId] =
114 | {success: successCallback, fail: failCallback};
115 | }
116 |
117 | actionArgs = massageArgsJsToNative(actionArgs);
118 |
119 | // CB-10133 DataClone DOM Exception 25 guard (fast function remover)
120 | var command = [callbackId, service, action, JSON.parse(JSON.stringify(actionArgs))];
121 | window.webkit.messageHandlers.cordova.postMessage(command);
122 | };
123 |
124 | iOSExec.nativeCallback = function (callbackId, status, message, keepCallback, debug) {
125 | var success = status === 0 || status === 1;
126 | var args = convertMessageToArgsNativeToJs(message);
127 | Promise.resolve().then(function () {
128 | cordova.callbackFromNative(callbackId, success, status, args, keepCallback); // eslint-disable-line
129 | });
130 | };
131 |
132 | // for backwards compatibility
133 | iOSExec.nativeEvalAndFetch = function (func) {
134 | try {
135 | func();
136 | } catch (e) {
137 | console.log(e);
138 | }
139 | };
140 |
141 | // Proxy the exec for bridge changes. See CB-10106
142 |
143 | function cordovaExec () {
144 | var cexec = require('cordova/exec');
145 | var cexec_valid = (typeof cexec.nativeFetchMessages === 'function') && (typeof cexec.nativeEvalAndFetch === 'function') && (typeof cexec.nativeCallback === 'function');
146 | return (cexec_valid && execProxy !== cexec) ? cexec : iOSExec;
147 | }
148 |
149 | function execProxy () {
150 | cordovaExec().apply(null, arguments);
151 | }
152 |
153 | execProxy.nativeFetchMessages = function () {
154 | return cordovaExec().nativeFetchMessages.apply(null, arguments);
155 | };
156 |
157 | execProxy.nativeEvalAndFetch = function () {
158 | return cordovaExec().nativeEvalAndFetch.apply(null, arguments);
159 | };
160 |
161 | execProxy.nativeCallback = function () {
162 | return cordovaExec().nativeCallback.apply(null, arguments);
163 | };
164 |
165 | module.exports = execProxy;
166 |
167 | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.cordova && window.webkit.messageHandlers.cordova.postMessage) {
168 | // unregister the old bridge
169 | cordova.define.remove('cordova/exec');
170 | // redefine bridge to our new bridge
171 | cordova.define('cordova/exec', function (require, exports, module) {
172 | module.exports = execProxy;
173 | });
174 | }
175 |
--------------------------------------------------------------------------------
/src/www/util.js:
--------------------------------------------------------------------------------
1 | var exec = require('cordova/exec');
2 |
3 | var WebView = {
4 | convertFileSrc: function(url) {
5 | if (!url) {
6 | return url;
7 | }
8 | if (url.indexOf('/')===0) {
9 | return window.WEBVIEW_SERVER_URL + '/_app_file_' + url;
10 | }
11 | if (url.indexOf('file://')===0) {
12 | return window.WEBVIEW_SERVER_URL + url.replace('file://', '/_app_file_');
13 | }
14 | if (url.indexOf('content://')===0) {
15 | return window.WEBVIEW_SERVER_URL + url.replace('content:/', '/_app_content_');
16 | }
17 | return url;
18 | },
19 | setServerBasePath: function(path) {
20 | exec(null, null, 'IonicWebView', 'setServerBasePath', [path]);
21 | },
22 | getServerBasePath: function(callback) {
23 | exec(callback, null, 'IonicWebView', 'getServerBasePath', []);
24 | },
25 | persistServerBasePath: function() {
26 | exec(null, null, 'IonicWebView', 'persistServerBasePath', []);
27 | }
28 | }
29 |
30 | module.exports = WebView;
31 |
--------------------------------------------------------------------------------