@capacitor-plus/android 8.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/capacitor/build.gradle +94 -0
  3. package/capacitor/lint-baseline.xml +136 -0
  4. package/capacitor/lint.xml +9 -0
  5. package/capacitor/proguard-rules.pro +28 -0
  6. package/capacitor/src/main/AndroidManifest.xml +3 -0
  7. package/capacitor/src/main/assets/native-bridge.js +1029 -0
  8. package/capacitor/src/main/java/com/getcapacitor/AndroidProtocolHandler.java +94 -0
  9. package/capacitor/src/main/java/com/getcapacitor/App.java +61 -0
  10. package/capacitor/src/main/java/com/getcapacitor/AppUUID.java +65 -0
  11. package/capacitor/src/main/java/com/getcapacitor/Bridge.java +1627 -0
  12. package/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java +212 -0
  13. package/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java +467 -0
  14. package/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java +105 -0
  15. package/capacitor/src/main/java/com/getcapacitor/CapConfig.java +709 -0
  16. package/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java +57 -0
  17. package/capacitor/src/main/java/com/getcapacitor/FileUtils.java +303 -0
  18. package/capacitor/src/main/java/com/getcapacitor/InvalidPluginException.java +8 -0
  19. package/capacitor/src/main/java/com/getcapacitor/InvalidPluginMethodException.java +16 -0
  20. package/capacitor/src/main/java/com/getcapacitor/JSArray.java +51 -0
  21. package/capacitor/src/main/java/com/getcapacitor/JSExport.java +208 -0
  22. package/capacitor/src/main/java/com/getcapacitor/JSExportException.java +16 -0
  23. package/capacitor/src/main/java/com/getcapacitor/JSInjector.java +127 -0
  24. package/capacitor/src/main/java/com/getcapacitor/JSObject.java +164 -0
  25. package/capacitor/src/main/java/com/getcapacitor/JSValue.java +65 -0
  26. package/capacitor/src/main/java/com/getcapacitor/Logger.java +103 -0
  27. package/capacitor/src/main/java/com/getcapacitor/MessageHandler.java +157 -0
  28. package/capacitor/src/main/java/com/getcapacitor/NativePlugin.java +37 -0
  29. package/capacitor/src/main/java/com/getcapacitor/PermissionState.java +31 -0
  30. package/capacitor/src/main/java/com/getcapacitor/Plugin.java +1050 -0
  31. package/capacitor/src/main/java/com/getcapacitor/PluginCall.java +394 -0
  32. package/capacitor/src/main/java/com/getcapacitor/PluginConfig.java +116 -0
  33. package/capacitor/src/main/java/com/getcapacitor/PluginHandle.java +160 -0
  34. package/capacitor/src/main/java/com/getcapacitor/PluginInvocationException.java +16 -0
  35. package/capacitor/src/main/java/com/getcapacitor/PluginLoadException.java +19 -0
  36. package/capacitor/src/main/java/com/getcapacitor/PluginManager.java +56 -0
  37. package/capacitor/src/main/java/com/getcapacitor/PluginMethod.java +15 -0
  38. package/capacitor/src/main/java/com/getcapacitor/PluginMethodHandle.java +33 -0
  39. package/capacitor/src/main/java/com/getcapacitor/PluginResult.java +84 -0
  40. package/capacitor/src/main/java/com/getcapacitor/ProcessedRoute.java +37 -0
  41. package/capacitor/src/main/java/com/getcapacitor/RouteProcessor.java +8 -0
  42. package/capacitor/src/main/java/com/getcapacitor/ServerPath.java +25 -0
  43. package/capacitor/src/main/java/com/getcapacitor/UriMatcher.java +184 -0
  44. package/capacitor/src/main/java/com/getcapacitor/WebViewListener.java +57 -0
  45. package/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java +758 -0
  46. package/capacitor/src/main/java/com/getcapacitor/annotation/ActivityCallback.java +10 -0
  47. package/capacitor/src/main/java/com/getcapacitor/annotation/CapacitorPlugin.java +35 -0
  48. package/capacitor/src/main/java/com/getcapacitor/annotation/Permission.java +22 -0
  49. package/capacitor/src/main/java/com/getcapacitor/annotation/PermissionCallback.java +10 -0
  50. package/capacitor/src/main/java/com/getcapacitor/cordova/CapacitorCordovaCookieManager.java +42 -0
  51. package/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaInterfaceImpl.java +39 -0
  52. package/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java +282 -0
  53. package/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java +239 -0
  54. package/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java +119 -0
  55. package/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java +119 -0
  56. package/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +220 -0
  57. package/capacitor/src/main/java/com/getcapacitor/plugin/WebView.java +48 -0
  58. package/capacitor/src/main/java/com/getcapacitor/plugin/util/AssetUtil.java +358 -0
  59. package/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java +473 -0
  60. package/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java +465 -0
  61. package/capacitor/src/main/java/com/getcapacitor/plugin/util/ICapacitorHttpUrlConnection.java +15 -0
  62. package/capacitor/src/main/java/com/getcapacitor/plugin/util/MimeType.java +17 -0
  63. package/capacitor/src/main/java/com/getcapacitor/util/HostMask.java +123 -0
  64. package/capacitor/src/main/java/com/getcapacitor/util/InternalUtils.java +27 -0
  65. package/capacitor/src/main/java/com/getcapacitor/util/JSONUtils.java +166 -0
  66. package/capacitor/src/main/java/com/getcapacitor/util/PermissionHelper.java +114 -0
  67. package/capacitor/src/main/java/com/getcapacitor/util/WebColor.java +28 -0
  68. package/capacitor/src/main/res/layout/capacitor_bridge_layout_main.xml +15 -0
  69. package/capacitor/src/main/res/layout/no_webview.xml +14 -0
  70. package/capacitor/src/main/res/values/colors.xml +6 -0
  71. package/capacitor/src/main/res/values/strings.xml +3 -0
  72. package/capacitor/src/main/res/values/styles.xml +6 -0
  73. package/package.json +31 -0
@@ -0,0 +1,758 @@
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.getcapacitor;
17
+
18
+ import static com.getcapacitor.plugin.util.HttpRequestHandler.isDomainExcludedFromSSL;
19
+
20
+ import android.content.Context;
21
+ import android.net.Uri;
22
+ import android.util.Base64;
23
+ import android.webkit.CookieManager;
24
+ import android.webkit.WebResourceRequest;
25
+ import android.webkit.WebResourceResponse;
26
+ import com.getcapacitor.plugin.util.CapacitorHttpUrlConnection;
27
+ import com.getcapacitor.plugin.util.HttpRequestHandler;
28
+ import java.io.IOException;
29
+ import java.io.InputStream;
30
+ import java.net.HttpURLConnection;
31
+ import java.net.URL;
32
+ import java.net.URLConnection;
33
+ import java.nio.charset.StandardCharsets;
34
+ import java.util.ArrayList;
35
+ import java.util.HashMap;
36
+ import java.util.LinkedHashMap;
37
+ import java.util.List;
38
+ import java.util.Map;
39
+
40
+ /**
41
+ * Helper class meant to be used with the android.webkit.WebView class to enable hosting assets,
42
+ * resources and other data on 'virtual' https:// URL.
43
+ * Hosting assets and resources on https:// URLs is desirable as it is compatible with the
44
+ * Same-Origin policy.
45
+ * <p>
46
+ * This class is intended to be used from within the
47
+ * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, String)} and
48
+ * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
49
+ * android.webkit.WebResourceRequest)}
50
+ * methods.
51
+ */
52
+ public class WebViewLocalServer {
53
+
54
+ private static final String capacitorFileStart = Bridge.CAPACITOR_FILE_START;
55
+ private static final String capacitorContentStart = Bridge.CAPACITOR_CONTENT_START;
56
+ private String basePath;
57
+
58
+ private final UriMatcher uriMatcher;
59
+ private final AndroidProtocolHandler protocolHandler;
60
+ private final ArrayList<String> authorities;
61
+ private boolean isAsset;
62
+ // Whether to route all requests to paths without extensions back to `index.html`
63
+ private final boolean html5mode;
64
+ private final JSInjector jsInjector;
65
+ private final Bridge bridge;
66
+
67
+ /**
68
+ * A handler that produces responses for paths on the virtual asset server.
69
+ * <p>
70
+ * Methods of this handler will be invoked on a background thread and care must be taken to
71
+ * correctly synchronize access to any shared state.
72
+ * <p>
73
+ * On Android KitKat and above these methods may be called on more than one thread. This thread
74
+ * may be different than the thread on which the shouldInterceptRequest method was invoke.
75
+ * This means that on Android KitKat and above it is possible to block in this method without
76
+ * blocking other resources from loading. The number of threads used to parallelize loading
77
+ * is an internal implementation detail of the WebView and may change between updates which
78
+ * means that the amount of time spend blocking in this method should be kept to an absolute
79
+ * minimum.
80
+ */
81
+ public abstract static class PathHandler {
82
+
83
+ protected String mimeType;
84
+ private String encoding;
85
+ private String charset;
86
+ private int statusCode;
87
+ private String reasonPhrase;
88
+ private Map<String, String> responseHeaders;
89
+
90
+ public PathHandler() {
91
+ this(null, null, 200, "OK", null);
92
+ }
93
+
94
+ public PathHandler(String encoding, String charset, int statusCode, String reasonPhrase, Map<String, String> responseHeaders) {
95
+ this.encoding = encoding;
96
+ this.charset = charset;
97
+ this.statusCode = statusCode;
98
+ this.reasonPhrase = reasonPhrase;
99
+ Map<String, String> tempResponseHeaders;
100
+ if (responseHeaders == null) {
101
+ tempResponseHeaders = new HashMap<>();
102
+ } else {
103
+ tempResponseHeaders = responseHeaders;
104
+ }
105
+ tempResponseHeaders.put("Cache-Control", "no-cache");
106
+ this.responseHeaders = tempResponseHeaders;
107
+ }
108
+
109
+ public InputStream handle(WebResourceRequest request) {
110
+ return handle(request.getUrl());
111
+ }
112
+
113
+ public abstract InputStream handle(Uri url);
114
+
115
+ public String getEncoding() {
116
+ return encoding;
117
+ }
118
+
119
+ public String getCharset() {
120
+ return charset;
121
+ }
122
+
123
+ public int getStatusCode() {
124
+ return statusCode;
125
+ }
126
+
127
+ public String getReasonPhrase() {
128
+ return reasonPhrase;
129
+ }
130
+
131
+ public Map<String, String> getResponseHeaders() {
132
+ return responseHeaders;
133
+ }
134
+ }
135
+
136
+ WebViewLocalServer(Context context, Bridge bridge, JSInjector jsInjector, ArrayList<String> authorities, boolean html5mode) {
137
+ uriMatcher = new UriMatcher(null);
138
+ this.html5mode = html5mode;
139
+ this.protocolHandler = new AndroidProtocolHandler(context.getApplicationContext());
140
+ this.authorities = authorities;
141
+ this.bridge = bridge;
142
+ this.jsInjector = jsInjector;
143
+ }
144
+
145
+ private static Uri parseAndVerifyUrl(String url) {
146
+ if (url == null) {
147
+ return null;
148
+ }
149
+ Uri uri = Uri.parse(url);
150
+ if (uri == null) {
151
+ Logger.error("Malformed URL: " + url);
152
+ return null;
153
+ }
154
+ String path = uri.getPath();
155
+ if (path == null || path.isEmpty()) {
156
+ Logger.error("URL does not have a path: " + url);
157
+ return null;
158
+ }
159
+ return uri;
160
+ }
161
+
162
+ /**
163
+ * Attempt to retrieve the WebResourceResponse associated with the given <code>request</code>.
164
+ * This method should be invoked from within
165
+ * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
166
+ * android.webkit.WebResourceRequest)}.
167
+ *
168
+ * @param request the request to process.
169
+ * @return a response if the request URL had a matching handler, null if no handler was found.
170
+ */
171
+ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
172
+ Uri loadingUrl = request.getUrl();
173
+
174
+ if (null != loadingUrl.getPath() && loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START)) {
175
+ Logger.debug("Handling CapacitorHttp request: " + loadingUrl);
176
+ try {
177
+ return handleCapacitorHttpRequest(request);
178
+ } catch (Exception e) {
179
+ Logger.error(e.getLocalizedMessage());
180
+ return null;
181
+ }
182
+ }
183
+
184
+ PathHandler handler;
185
+ synchronized (uriMatcher) {
186
+ handler = (PathHandler) uriMatcher.match(request.getUrl());
187
+ }
188
+ if (handler == null) {
189
+ return null;
190
+ }
191
+
192
+ if (isLocalFile(loadingUrl) || isMainUrl(loadingUrl) || !isAllowedUrl(loadingUrl) || isErrorUrl(loadingUrl)) {
193
+ Logger.debug("Handling local request: " + request.getUrl().toString());
194
+ return handleLocalRequest(request, handler);
195
+ } else {
196
+ return handleProxyRequest(request, handler);
197
+ }
198
+ }
199
+
200
+ private boolean isLocalFile(Uri uri) {
201
+ String path = uri.getPath();
202
+ return path.startsWith(capacitorContentStart) || path.startsWith(capacitorFileStart);
203
+ }
204
+
205
+ private boolean isErrorUrl(Uri uri) {
206
+ String url = uri.toString();
207
+ return url.equals(bridge.getErrorUrl());
208
+ }
209
+
210
+ private boolean isMainUrl(Uri loadingUrl) {
211
+ return (bridge.getServerUrl() == null && loadingUrl.getHost().equalsIgnoreCase(bridge.getHost()));
212
+ }
213
+
214
+ private boolean isAllowedUrl(Uri loadingUrl) {
215
+ return !(bridge.getServerUrl() == null && !bridge.getAppAllowNavigationMask().matches(loadingUrl.getHost()));
216
+ }
217
+
218
+ private String getReasonPhraseFromResponseCode(int code) {
219
+ return switch (code) {
220
+ case 100 -> "Continue";
221
+ case 101 -> "Switching Protocols";
222
+ case 200 -> "OK";
223
+ case 201 -> "Created";
224
+ case 202 -> "Accepted";
225
+ case 203 -> "Non-Authoritative Information";
226
+ case 204 -> "No Content";
227
+ case 205 -> "Reset Content";
228
+ case 206 -> "Partial Content";
229
+ case 300 -> "Multiple Choices";
230
+ case 301 -> "Moved Permanently";
231
+ case 302 -> "Found";
232
+ case 303 -> "See Other";
233
+ case 304 -> "Not Modified";
234
+ case 400 -> "Bad Request";
235
+ case 401 -> "Unauthorized";
236
+ case 403 -> "Forbidden";
237
+ case 404 -> "Not Found";
238
+ case 405 -> "Method Not Allowed";
239
+ case 406 -> "Not Acceptable";
240
+ case 407 -> "Proxy Authentication Required";
241
+ case 408 -> "Request Timeout";
242
+ case 409 -> "Conflict";
243
+ case 410 -> "Gone";
244
+ case 500 -> "Internal Server Error";
245
+ case 501 -> "Not Implemented";
246
+ case 502 -> "Bad Gateway";
247
+ case 503 -> "Service Unavailable";
248
+ case 504 -> "Gateway Timeout";
249
+ case 505 -> "HTTP Version Not Supported";
250
+ default -> "Unknown";
251
+ };
252
+ }
253
+
254
+ private WebResourceResponse handleCapacitorHttpRequest(WebResourceRequest request) throws IOException {
255
+ String urlString = request.getUrl().getQueryParameter(Bridge.CAPACITOR_HTTP_INTERCEPTOR_URL_PARAM);
256
+ URL url = new URL(urlString);
257
+ JSObject headers = new JSObject();
258
+
259
+ for (Map.Entry<String, String> header : request.getRequestHeaders().entrySet()) {
260
+ headers.put(header.getKey(), header.getValue());
261
+ }
262
+
263
+ // a workaround for the following android web view issue:
264
+ // https://issues.chromium.org/issues/40450316
265
+ // x-cap-user-agent contains the user agent set in JavaScript
266
+ String userAgentValue = headers.getString("x-cap-user-agent");
267
+ if (userAgentValue != null) {
268
+ headers.put("User-Agent", userAgentValue);
269
+ }
270
+ headers.remove("x-cap-user-agent");
271
+
272
+ HttpRequestHandler.HttpURLConnectionBuilder connectionBuilder = new HttpRequestHandler.HttpURLConnectionBuilder()
273
+ .setUrl(url)
274
+ .setMethod(request.getMethod())
275
+ .setHeaders(headers)
276
+ .openConnection();
277
+
278
+ CapacitorHttpUrlConnection connection = connectionBuilder.build();
279
+
280
+ if (!isDomainExcludedFromSSL(bridge, url)) {
281
+ connection.setSSLSocketFactory(bridge);
282
+ }
283
+
284
+ connection.connect();
285
+
286
+ String mimeType = null;
287
+ String encoding = null;
288
+ Map<String, String> responseHeaders = new LinkedHashMap<>();
289
+ for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
290
+ StringBuilder builder = new StringBuilder();
291
+ for (String value : entry.getValue()) {
292
+ builder.append(value);
293
+ builder.append(", ");
294
+ }
295
+ builder.setLength(builder.length() - 2);
296
+
297
+ if ("Content-Type".equalsIgnoreCase(entry.getKey())) {
298
+ String[] contentTypeParts = builder.toString().split(";");
299
+ mimeType = contentTypeParts[0].trim();
300
+ if (contentTypeParts.length > 1) {
301
+ String[] encodingParts = contentTypeParts[1].split("=");
302
+ if (encodingParts.length > 1) {
303
+ encoding = encodingParts[1].trim();
304
+ }
305
+ }
306
+ } else {
307
+ responseHeaders.put(entry.getKey(), builder.toString());
308
+ }
309
+ }
310
+
311
+ InputStream inputStream = connection.getErrorStream();
312
+ if (inputStream == null) {
313
+ inputStream = connection.getInputStream();
314
+ }
315
+
316
+ if (null == mimeType) {
317
+ mimeType = getMimeType(request.getUrl().getPath(), inputStream);
318
+ }
319
+
320
+ int responseCode = connection.getResponseCode();
321
+ String reasonPhrase = getReasonPhraseFromResponseCode(responseCode);
322
+
323
+ return new WebResourceResponse(mimeType, encoding, responseCode, reasonPhrase, responseHeaders, inputStream);
324
+ }
325
+
326
+ private WebResourceResponse handleLocalRequest(WebResourceRequest request, PathHandler handler) {
327
+ String path = request.getUrl().getPath();
328
+
329
+ if (request.getRequestHeaders().get("Range") != null) {
330
+ InputStream responseStream = new LollipopLazyInputStream(handler, request);
331
+ String mimeType = getMimeType(path, responseStream);
332
+ Map<String, String> tempResponseHeaders = handler.getResponseHeaders();
333
+ int statusCode = 206;
334
+ try {
335
+ int totalRange = responseStream.available();
336
+ String rangeString = request.getRequestHeaders().get("Range");
337
+ String[] parts = rangeString.split("=");
338
+ String[] streamParts = parts[1].split("-");
339
+ String fromRange = streamParts[0];
340
+ int range = totalRange - 1;
341
+ if (streamParts.length > 1) {
342
+ range = Integer.parseInt(streamParts[1]);
343
+ }
344
+ tempResponseHeaders.put("Accept-Ranges", "bytes");
345
+ tempResponseHeaders.put("Content-Range", "bytes " + fromRange + "-" + range + "/" + totalRange);
346
+ } catch (IOException e) {
347
+ statusCode = 404;
348
+ }
349
+ return new WebResourceResponse(
350
+ mimeType,
351
+ handler.getEncoding(),
352
+ statusCode,
353
+ handler.getReasonPhrase(),
354
+ tempResponseHeaders,
355
+ responseStream
356
+ );
357
+ }
358
+
359
+ if (isLocalFile(request.getUrl()) || isErrorUrl(request.getUrl())) {
360
+ InputStream responseStream = new LollipopLazyInputStream(handler, request);
361
+ String mimeType = getMimeType(request.getUrl().getPath(), responseStream);
362
+ int statusCode = getStatusCode(responseStream, handler.getStatusCode());
363
+ return new WebResourceResponse(
364
+ mimeType,
365
+ handler.getEncoding(),
366
+ statusCode,
367
+ handler.getReasonPhrase(),
368
+ handler.getResponseHeaders(),
369
+ responseStream
370
+ );
371
+ }
372
+
373
+ if (path.equals("/cordova.js")) {
374
+ return new WebResourceResponse(
375
+ "application/javascript",
376
+ handler.getEncoding(),
377
+ handler.getStatusCode(),
378
+ handler.getReasonPhrase(),
379
+ handler.getResponseHeaders(),
380
+ null
381
+ );
382
+ }
383
+
384
+ if (path.equals("/") || (!request.getUrl().getLastPathSegment().contains(".") && html5mode)) {
385
+ InputStream responseStream;
386
+ try {
387
+ String startPath = this.basePath + "/index.html";
388
+ if (bridge.getRouteProcessor() != null) {
389
+ ProcessedRoute processedRoute = bridge.getRouteProcessor().process(this.basePath, "/index.html");
390
+ startPath = processedRoute.getPath();
391
+ isAsset = processedRoute.isAsset();
392
+ }
393
+
394
+ if (isAsset) {
395
+ responseStream = protocolHandler.openAsset(startPath);
396
+ } else {
397
+ responseStream = protocolHandler.openFile(startPath);
398
+ }
399
+ } catch (IOException e) {
400
+ Logger.error("Unable to open index.html", e);
401
+ return null;
402
+ }
403
+
404
+ if (jsInjector != null) {
405
+ responseStream = jsInjector.getInjectedStream(responseStream);
406
+ }
407
+
408
+ int statusCode = getStatusCode(responseStream, handler.getStatusCode());
409
+ return new WebResourceResponse(
410
+ "text/html",
411
+ handler.getEncoding(),
412
+ statusCode,
413
+ handler.getReasonPhrase(),
414
+ handler.getResponseHeaders(),
415
+ responseStream
416
+ );
417
+ }
418
+
419
+ if ("/favicon.ico".equalsIgnoreCase(path)) {
420
+ try {
421
+ return new WebResourceResponse("image/png", null, null);
422
+ } catch (Exception e) {
423
+ Logger.error("favicon handling failed", e);
424
+ }
425
+ }
426
+
427
+ int periodIndex = path.lastIndexOf(".");
428
+ if (periodIndex >= 0) {
429
+ String ext = path.substring(path.lastIndexOf("."));
430
+
431
+ InputStream responseStream = new LollipopLazyInputStream(handler, request);
432
+
433
+ // TODO: Conjure up a bit more subtlety than this
434
+ if (ext.equals(".html") && jsInjector != null) {
435
+ responseStream = jsInjector.getInjectedStream(responseStream);
436
+ }
437
+
438
+ String mimeType = getMimeType(path, responseStream);
439
+ int statusCode = getStatusCode(responseStream, handler.getStatusCode());
440
+ return new WebResourceResponse(
441
+ mimeType,
442
+ handler.getEncoding(),
443
+ statusCode,
444
+ handler.getReasonPhrase(),
445
+ handler.getResponseHeaders(),
446
+ responseStream
447
+ );
448
+ }
449
+
450
+ return null;
451
+ }
452
+
453
+ /**
454
+ * Prepends an {@code InputStream} with the JavaScript required by Capacitor.
455
+ * This method only changes the original {@code InputStream} if {@code WebView} does not
456
+ * support the {@code DOCUMENT_START_SCRIPT} feature.
457
+ * @param original the original {@code InputStream}
458
+ * @return the modified {@code InputStream}
459
+ */
460
+ public InputStream getJavaScriptInjectedStream(InputStream original) {
461
+ if (jsInjector != null) {
462
+ return jsInjector.getInjectedStream(original);
463
+ }
464
+ return original;
465
+ }
466
+
467
+ /**
468
+ * Instead of reading files from the filesystem/assets, proxy through to the URL
469
+ * and let an external server handle it.
470
+ * @param request
471
+ * @param handler
472
+ * @return
473
+ */
474
+ private WebResourceResponse handleProxyRequest(WebResourceRequest request, PathHandler handler) {
475
+ if (jsInjector != null) {
476
+ final String method = request.getMethod();
477
+ if (method.equals("GET")) {
478
+ try {
479
+ String url = request.getUrl().toString();
480
+ Map<String, String> headers = request.getRequestHeaders();
481
+ boolean isHtmlText = false;
482
+ for (Map.Entry<String, String> header : headers.entrySet()) {
483
+ if (header.getKey().equalsIgnoreCase("Accept") && header.getValue().toLowerCase().contains("text/html")) {
484
+ isHtmlText = true;
485
+ break;
486
+ }
487
+ }
488
+ if (isHtmlText) {
489
+ HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
490
+ for (Map.Entry<String, String> header : headers.entrySet()) {
491
+ conn.setRequestProperty(header.getKey(), header.getValue());
492
+ }
493
+ String getCookie = CookieManager.getInstance().getCookie(url);
494
+ if (getCookie != null) {
495
+ conn.setRequestProperty("Cookie", getCookie);
496
+ }
497
+ conn.setRequestMethod(method);
498
+ conn.setReadTimeout(30 * 1000);
499
+ conn.setConnectTimeout(30 * 1000);
500
+ if (request.getUrl().getUserInfo() != null) {
501
+ byte[] userInfoBytes = request.getUrl().getUserInfo().getBytes(StandardCharsets.UTF_8);
502
+ String base64 = Base64.encodeToString(userInfoBytes, Base64.NO_WRAP);
503
+ conn.setRequestProperty("Authorization", "Basic " + base64);
504
+ }
505
+
506
+ List<String> cookies = conn.getHeaderFields().get("Set-Cookie");
507
+ if (cookies != null) {
508
+ for (String cookie : cookies) {
509
+ CookieManager.getInstance().setCookie(url, cookie);
510
+ }
511
+ }
512
+ InputStream responseStream = conn.getInputStream();
513
+ responseStream = jsInjector.getInjectedStream(responseStream);
514
+
515
+ return new WebResourceResponse(
516
+ "text/html",
517
+ handler.getEncoding(),
518
+ handler.getStatusCode(),
519
+ handler.getReasonPhrase(),
520
+ handler.getResponseHeaders(),
521
+ responseStream
522
+ );
523
+ }
524
+ } catch (Exception ex) {
525
+ bridge.handleAppUrlLoadError(ex);
526
+ }
527
+ }
528
+ }
529
+ return null;
530
+ }
531
+
532
+ private String getMimeType(String path, InputStream stream) {
533
+ String mimeType = null;
534
+ try {
535
+ mimeType = URLConnection.guessContentTypeFromName(path); // Does not recognize *.js
536
+ if (mimeType != null && path.endsWith(".js") && mimeType.equals("image/x-icon")) {
537
+ Logger.debug("We shouldn't be here");
538
+ }
539
+ if (mimeType == null) {
540
+ if (path.endsWith(".js") || path.endsWith(".mjs")) {
541
+ // Make sure JS files get the proper mimetype to support ES modules
542
+ mimeType = "application/javascript";
543
+ } else if (path.endsWith(".wasm")) {
544
+ mimeType = "application/wasm";
545
+ } else {
546
+ mimeType = URLConnection.guessContentTypeFromStream(stream);
547
+ }
548
+ }
549
+ } catch (Exception ex) {
550
+ Logger.error("Unable to get mime type" + path, ex);
551
+ }
552
+ return mimeType;
553
+ }
554
+
555
+ private int getStatusCode(InputStream stream, int defaultCode) {
556
+ int finalStatusCode = defaultCode;
557
+ try {
558
+ if (stream.available() == -1) {
559
+ finalStatusCode = 404;
560
+ }
561
+ } catch (IOException e) {
562
+ finalStatusCode = 500;
563
+ }
564
+ return finalStatusCode;
565
+ }
566
+
567
+ /**
568
+ * Registers a handler for the given <code>uri</code>. The <code>handler</code> will be invoked
569
+ * every time the <code>shouldInterceptRequest</code> method of the instance is called with
570
+ * a matching <code>uri</code>.
571
+ *
572
+ * @param uri the uri to use the handler for. The scheme and authority (domain) will be matched
573
+ * exactly. The path may contain a '*' element which will match a single element of
574
+ * a path (so a handler registered for /a/* will be invoked for /a/b and /a/c.html
575
+ * but not for /a/b/b) or the '**' element which will match any number of path
576
+ * elements.
577
+ * @param handler the handler to use for the uri.
578
+ */
579
+ void register(Uri uri, PathHandler handler) {
580
+ synchronized (uriMatcher) {
581
+ uriMatcher.addURI(uri.getScheme(), uri.getAuthority(), uri.getPath(), handler);
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Hosts the application's assets on an https:// URL. Assets from the local path
587
+ * <code>assetPath/...</code> will be available under
588
+ * <code>https://{uuid}.androidplatform.net/assets/...</code>.
589
+ *
590
+ * @param assetPath the local path in the application's asset folder which will be made
591
+ * available by the server (for example "/www").
592
+ * @return prefixes under which the assets are hosted.
593
+ */
594
+ public void hostAssets(String assetPath) {
595
+ this.isAsset = true;
596
+ this.basePath = assetPath;
597
+ createHostingDetails();
598
+ }
599
+
600
+ /**
601
+ * Hosts the application's files on an https:// URL. Files from the basePath
602
+ * <code>basePath/...</code> will be available under
603
+ * <code>https://{uuid}.androidplatform.net/...</code>.
604
+ *
605
+ * @param basePath the local path in the application's data folder which will be made
606
+ * available by the server (for example "/www").
607
+ * @return prefixes under which the assets are hosted.
608
+ */
609
+ public void hostFiles(final String basePath) {
610
+ this.isAsset = false;
611
+ this.basePath = basePath;
612
+ createHostingDetails();
613
+ }
614
+
615
+ private void createHostingDetails() {
616
+ final String assetPath = this.basePath;
617
+
618
+ if (assetPath.indexOf('*') != -1) {
619
+ throw new IllegalArgumentException("assetPath cannot contain the '*' character.");
620
+ }
621
+
622
+ PathHandler handler = new PathHandler() {
623
+ @Override
624
+ public InputStream handle(Uri url) {
625
+ InputStream stream = null;
626
+ String path = url.getPath();
627
+
628
+ // Pass path to routeProcessor if present
629
+ RouteProcessor routeProcessor = bridge.getRouteProcessor();
630
+ boolean ignoreAssetPath = false;
631
+ if (routeProcessor != null) {
632
+ ProcessedRoute processedRoute = bridge.getRouteProcessor().process("", path);
633
+ path = processedRoute.getPath();
634
+ isAsset = processedRoute.isAsset();
635
+ ignoreAssetPath = processedRoute.isIgnoreAssetPath();
636
+ }
637
+
638
+ try {
639
+ if (path.startsWith(capacitorContentStart)) {
640
+ stream = protocolHandler.openContentUrl(url);
641
+ } else if (path.startsWith(capacitorFileStart)) {
642
+ stream = protocolHandler.openFile(path);
643
+ } else if (!isAsset) {
644
+ if (routeProcessor == null) {
645
+ path = basePath + url.getPath();
646
+ }
647
+
648
+ stream = protocolHandler.openFile(path);
649
+ } else if (ignoreAssetPath) {
650
+ stream = protocolHandler.openAsset(path);
651
+ } else {
652
+ stream = protocolHandler.openAsset(assetPath + path);
653
+ }
654
+ } catch (IOException e) {
655
+ Logger.error("Unable to open asset URL: " + url);
656
+ return null;
657
+ }
658
+
659
+ return stream;
660
+ }
661
+ };
662
+
663
+ for (String authority : authorities) {
664
+ registerUriForScheme(Bridge.CAPACITOR_HTTP_SCHEME, handler, authority);
665
+ registerUriForScheme(Bridge.CAPACITOR_HTTPS_SCHEME, handler, authority);
666
+
667
+ String customScheme = this.bridge.getScheme();
668
+ if (!customScheme.equals(Bridge.CAPACITOR_HTTP_SCHEME) && !customScheme.equals(Bridge.CAPACITOR_HTTPS_SCHEME)) {
669
+ registerUriForScheme(customScheme, handler, authority);
670
+ }
671
+ }
672
+ }
673
+
674
+ private void registerUriForScheme(String scheme, PathHandler handler, String authority) {
675
+ Uri.Builder uriBuilder = new Uri.Builder();
676
+ uriBuilder.scheme(scheme);
677
+ uriBuilder.authority(authority);
678
+ uriBuilder.path("");
679
+ Uri uriPrefix = uriBuilder.build();
680
+
681
+ register(Uri.withAppendedPath(uriPrefix, "/"), handler);
682
+ register(Uri.withAppendedPath(uriPrefix, "**"), handler);
683
+ }
684
+
685
+ /**
686
+ * The KitKat WebView reads the InputStream on a separate threadpool. We can use that to
687
+ * parallelize loading.
688
+ */
689
+ private abstract static class LazyInputStream extends InputStream {
690
+
691
+ protected final PathHandler handler;
692
+ private InputStream is = null;
693
+
694
+ public LazyInputStream(PathHandler handler) {
695
+ this.handler = handler;
696
+ }
697
+
698
+ private InputStream getInputStream() {
699
+ if (is == null) {
700
+ is = handle();
701
+ }
702
+ return is;
703
+ }
704
+
705
+ protected abstract InputStream handle();
706
+
707
+ @Override
708
+ public int available() throws IOException {
709
+ InputStream is = getInputStream();
710
+ return (is != null) ? is.available() : -1;
711
+ }
712
+
713
+ @Override
714
+ public int read() throws IOException {
715
+ InputStream is = getInputStream();
716
+ return (is != null) ? is.read() : -1;
717
+ }
718
+
719
+ @Override
720
+ public int read(byte[] b) throws IOException {
721
+ InputStream is = getInputStream();
722
+ return (is != null) ? is.read(b) : -1;
723
+ }
724
+
725
+ @Override
726
+ public int read(byte[] b, int off, int len) throws IOException {
727
+ InputStream is = getInputStream();
728
+ return (is != null) ? is.read(b, off, len) : -1;
729
+ }
730
+
731
+ @Override
732
+ public long skip(long n) throws IOException {
733
+ InputStream is = getInputStream();
734
+ return (is != null) ? is.skip(n) : 0;
735
+ }
736
+ }
737
+
738
+ // For L and above.
739
+ private static class LollipopLazyInputStream extends LazyInputStream {
740
+
741
+ private WebResourceRequest request;
742
+ private InputStream is;
743
+
744
+ public LollipopLazyInputStream(PathHandler handler, WebResourceRequest request) {
745
+ super(handler);
746
+ this.request = request;
747
+ }
748
+
749
+ @Override
750
+ protected InputStream handle() {
751
+ return handler.handle(request);
752
+ }
753
+ }
754
+
755
+ public String getBasePath() {
756
+ return this.basePath;
757
+ }
758
+ }