@capgo/inappbrowser 8.0.0 → 8.0.1
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.
- package/CapgoInappbrowser.podspec +2 -2
- package/LICENSE +373 -21
- package/Package.swift +28 -0
- package/README.md +600 -74
- package/android/build.gradle +17 -16
- package/android/src/main/AndroidManifest.xml +14 -2
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java +952 -204
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +478 -81
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewCallbacks.java +10 -4
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +3023 -226
- package/android/src/main/res/drawable/ic_refresh.xml +9 -0
- package/android/src/main/res/drawable/ic_share.xml +10 -0
- package/android/src/main/res/layout/activity_browser.xml +10 -0
- package/android/src/main/res/layout/content_browser.xml +3 -2
- package/android/src/main/res/layout/tool_bar.xml +44 -7
- package/android/src/main/res/values/strings.xml +4 -0
- package/android/src/main/res/values/themes.xml +27 -0
- package/android/src/main/res/xml/file_paths.xml +14 -0
- package/dist/docs.json +1289 -149
- package/dist/esm/definitions.d.ts +614 -25
- package/dist/esm/definitions.js +17 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +4 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +16 -3
- package/dist/esm/web.js +43 -7
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +60 -8
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +60 -8
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +952 -0
- package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +2006 -0
- package/package.json +30 -30
- package/ios/Plugin/Assets.xcassets/Back.imageset/Back.png +0 -0
- package/ios/Plugin/Assets.xcassets/Back.imageset/Back@2x.png +0 -0
- package/ios/Plugin/Assets.xcassets/Back.imageset/Back@3x.png +0 -0
- package/ios/Plugin/Assets.xcassets/Back.imageset/Contents.json +0 -26
- package/ios/Plugin/Assets.xcassets/Contents.json +0 -6
- package/ios/Plugin/Assets.xcassets/Forward.imageset/Contents.json +0 -26
- package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward.png +0 -0
- package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@2x.png +0 -0
- package/ios/Plugin/Assets.xcassets/Forward.imageset/Forward@3x.png +0 -0
- package/ios/Plugin/InAppBrowserPlugin.h +0 -10
- package/ios/Plugin/InAppBrowserPlugin.m +0 -17
- package/ios/Plugin/InAppBrowserPlugin.swift +0 -203
- package/ios/Plugin/Info.plist +0 -24
- package/ios/Plugin/WKWebViewController.swift +0 -784
- /package/ios/{Plugin → Sources/InAppBrowserPlugin}/Enums.swift +0 -0
|
@@ -1,262 +1,3059 @@
|
|
|
1
1
|
package ee.forgr.capacitor_inappbrowser;
|
|
2
2
|
|
|
3
|
+
import android.annotation.SuppressLint;
|
|
4
|
+
import android.app.Activity;
|
|
5
|
+
import android.app.AlertDialog;
|
|
3
6
|
import android.app.Dialog;
|
|
7
|
+
import android.content.ActivityNotFoundException;
|
|
4
8
|
import android.content.Context;
|
|
9
|
+
import android.content.DialogInterface;
|
|
10
|
+
import android.content.Intent;
|
|
11
|
+
import android.content.res.AssetManager;
|
|
12
|
+
import android.content.res.Resources;
|
|
5
13
|
import android.graphics.Bitmap;
|
|
14
|
+
import android.graphics.Canvas;
|
|
15
|
+
import android.graphics.Color;
|
|
16
|
+
import android.graphics.Paint;
|
|
17
|
+
import android.graphics.PorterDuff;
|
|
18
|
+
import android.graphics.PorterDuffColorFilter;
|
|
19
|
+
import android.net.Uri;
|
|
20
|
+
import android.net.http.SslError;
|
|
21
|
+
import android.os.Build;
|
|
22
|
+
import android.os.Environment;
|
|
23
|
+
import android.print.PrintAttributes;
|
|
24
|
+
import android.print.PrintDocumentAdapter;
|
|
25
|
+
import android.print.PrintManager;
|
|
26
|
+
import android.provider.MediaStore;
|
|
27
|
+
import android.security.KeyChain;
|
|
28
|
+
import android.security.KeyChainAliasCallback;
|
|
6
29
|
import android.text.TextUtils;
|
|
30
|
+
import android.util.Base64;
|
|
31
|
+
import android.util.Log;
|
|
32
|
+
import android.util.TypedValue;
|
|
33
|
+
import android.view.KeyEvent;
|
|
7
34
|
import android.view.View;
|
|
35
|
+
import android.view.ViewGroup;
|
|
8
36
|
import android.view.Window;
|
|
9
37
|
import android.view.WindowManager;
|
|
38
|
+
import android.webkit.HttpAuthHandler;
|
|
39
|
+
import android.webkit.JavascriptInterface;
|
|
40
|
+
import android.webkit.PermissionRequest;
|
|
41
|
+
import android.webkit.SslErrorHandler;
|
|
42
|
+
import android.webkit.ValueCallback;
|
|
43
|
+
import android.webkit.WebChromeClient;
|
|
10
44
|
import android.webkit.WebResourceError;
|
|
11
45
|
import android.webkit.WebResourceRequest;
|
|
46
|
+
import android.webkit.WebResourceResponse;
|
|
12
47
|
import android.webkit.WebView;
|
|
13
48
|
import android.webkit.WebViewClient;
|
|
14
49
|
import android.widget.ImageButton;
|
|
50
|
+
import android.widget.ImageView;
|
|
15
51
|
import android.widget.TextView;
|
|
52
|
+
import android.widget.Toast;
|
|
16
53
|
import android.widget.Toolbar;
|
|
54
|
+
import androidx.activity.result.ActivityResult;
|
|
55
|
+
import androidx.activity.result.contract.ActivityResultContracts;
|
|
56
|
+
import androidx.core.content.FileProvider;
|
|
57
|
+
import androidx.core.graphics.Insets;
|
|
58
|
+
import androidx.core.view.ViewCompat;
|
|
59
|
+
import androidx.core.view.WindowInsetsCompat;
|
|
60
|
+
import androidx.core.view.WindowInsetsControllerCompat;
|
|
61
|
+
import androidx.webkit.WebSettingsCompat;
|
|
62
|
+
import androidx.webkit.WebViewFeature;
|
|
63
|
+
import com.caverock.androidsvg.SVG;
|
|
64
|
+
import com.caverock.androidsvg.SVGParseException;
|
|
65
|
+
import com.getcapacitor.JSObject;
|
|
66
|
+
import java.io.ByteArrayInputStream;
|
|
67
|
+
import java.io.File;
|
|
68
|
+
import java.io.IOException;
|
|
69
|
+
import java.io.InputStream;
|
|
17
70
|
import java.net.URI;
|
|
18
71
|
import java.net.URISyntaxException;
|
|
72
|
+
import java.nio.charset.StandardCharsets;
|
|
73
|
+
import java.security.PrivateKey;
|
|
74
|
+
import java.security.cert.X509Certificate;
|
|
75
|
+
import java.util.Arrays;
|
|
19
76
|
import java.util.HashMap;
|
|
20
77
|
import java.util.Iterator;
|
|
78
|
+
import java.util.List;
|
|
21
79
|
import java.util.Map;
|
|
80
|
+
import java.util.Objects;
|
|
81
|
+
import java.util.UUID;
|
|
82
|
+
import java.util.concurrent.ExecutorService;
|
|
83
|
+
import java.util.concurrent.Executors;
|
|
84
|
+
import java.util.concurrent.Semaphore;
|
|
85
|
+
import java.util.concurrent.TimeUnit;
|
|
86
|
+
import java.util.regex.Matcher;
|
|
87
|
+
import java.util.regex.Pattern;
|
|
88
|
+
import org.json.JSONException;
|
|
89
|
+
import org.json.JSONObject;
|
|
22
90
|
|
|
23
91
|
public class WebViewDialog extends Dialog {
|
|
24
92
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
private static class ProxiedRequest {
|
|
94
|
+
|
|
95
|
+
private WebResourceResponse response;
|
|
96
|
+
private final Semaphore semaphore;
|
|
97
|
+
|
|
98
|
+
public WebResourceResponse getResponse() {
|
|
99
|
+
return response;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public ProxiedRequest() {
|
|
103
|
+
this.semaphore = new Semaphore(0);
|
|
104
|
+
this.response = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private WebView _webView;
|
|
109
|
+
private Toolbar _toolbar;
|
|
110
|
+
private Options _options = null;
|
|
111
|
+
private final Context _context;
|
|
112
|
+
public Activity activity;
|
|
113
|
+
private boolean isInitialized = false;
|
|
114
|
+
private boolean datePickerInjected = false; // Track if we've injected date picker fixes
|
|
115
|
+
private final WebView capacitorWebView;
|
|
116
|
+
private final Map<String, ProxiedRequest> proxiedRequestsHashmap = new HashMap<>();
|
|
117
|
+
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
|
118
|
+
private int iconColor = Color.BLACK; // Default icon color
|
|
119
|
+
|
|
120
|
+
Semaphore preShowSemaphore = null;
|
|
121
|
+
String preShowError = null;
|
|
122
|
+
|
|
123
|
+
public PermissionRequest currentPermissionRequest;
|
|
124
|
+
public static final int FILE_CHOOSER_REQUEST_CODE = 1000;
|
|
125
|
+
public ValueCallback<Uri> mUploadMessage;
|
|
126
|
+
public ValueCallback<Uri[]> mFilePathCallback;
|
|
127
|
+
|
|
128
|
+
// Temporary URI for storing camera capture
|
|
129
|
+
public Uri tempCameraUri;
|
|
130
|
+
|
|
131
|
+
public interface PermissionHandler {
|
|
132
|
+
void handleCameraPermissionRequest(PermissionRequest request);
|
|
133
|
+
|
|
134
|
+
void handleMicrophonePermissionRequest(PermissionRequest request);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private final PermissionHandler permissionHandler;
|
|
138
|
+
|
|
139
|
+
public WebViewDialog(Context context, int theme, Options options, PermissionHandler permissionHandler, WebView capacitorWebView) {
|
|
140
|
+
// Use Material theme only if materialPicker is enabled
|
|
141
|
+
super(context, options.getMaterialPicker() ? R.style.InAppBrowserMaterialTheme : theme);
|
|
142
|
+
this._options = options;
|
|
143
|
+
this._context = context;
|
|
144
|
+
this.permissionHandler = permissionHandler;
|
|
145
|
+
this.isInitialized = false;
|
|
146
|
+
this.capacitorWebView = capacitorWebView;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Add this class to provide safer JavaScript interface
|
|
150
|
+
private class JavaScriptInterface {
|
|
151
|
+
|
|
152
|
+
@JavascriptInterface
|
|
153
|
+
public void postMessage(String message) {
|
|
154
|
+
try {
|
|
155
|
+
// Handle message from JavaScript safely
|
|
156
|
+
if (message == null || message.isEmpty()) {
|
|
157
|
+
Log.e("InAppBrowser", "Received empty message from WebView");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (_options == null || _options.getCallbacks() == null) {
|
|
162
|
+
Log.e("InAppBrowser", "Cannot handle postMessage - options or callbacks are null");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_options.getCallbacks().javascriptCallback(message);
|
|
167
|
+
} catch (Exception e) {
|
|
168
|
+
Log.e("InAppBrowser", "Error in postMessage: " + e.getMessage());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@JavascriptInterface
|
|
173
|
+
public void close() {
|
|
174
|
+
try {
|
|
175
|
+
// close webview safely
|
|
176
|
+
if (activity == null) {
|
|
177
|
+
Log.e("InAppBrowser", "Cannot close - activity is null");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
activity.runOnUiThread(() -> {
|
|
182
|
+
try {
|
|
183
|
+
String currentUrl = getUrl();
|
|
184
|
+
dismiss();
|
|
185
|
+
|
|
186
|
+
if (_options != null && _options.getCallbacks() != null) {
|
|
187
|
+
_options.getCallbacks().closeEvent(currentUrl);
|
|
188
|
+
}
|
|
189
|
+
} catch (Exception e) {
|
|
190
|
+
Log.e("InAppBrowser", "Error closing WebView: " + e.getMessage());
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
} catch (Exception e) {
|
|
194
|
+
Log.e("InAppBrowser", "Error in close: " + e.getMessage());
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public class PreShowScriptInterface {
|
|
200
|
+
|
|
201
|
+
@JavascriptInterface
|
|
202
|
+
public void error(String error) {
|
|
203
|
+
try {
|
|
204
|
+
// Handle message from JavaScript
|
|
205
|
+
if (preShowSemaphore != null) {
|
|
206
|
+
preShowError = error;
|
|
207
|
+
preShowSemaphore.release();
|
|
208
|
+
}
|
|
209
|
+
} catch (Exception e) {
|
|
210
|
+
Log.e("InAppBrowser", "Error in error callback: " + e.getMessage());
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@JavascriptInterface
|
|
215
|
+
public void success() {
|
|
216
|
+
try {
|
|
217
|
+
// Handle message from JavaScript
|
|
218
|
+
if (preShowSemaphore != null) {
|
|
219
|
+
preShowSemaphore.release();
|
|
220
|
+
}
|
|
221
|
+
} catch (Exception e) {
|
|
222
|
+
Log.e("InAppBrowser", "Error in success callback: " + e.getMessage());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public class PrintInterface {
|
|
228
|
+
|
|
229
|
+
private Context context;
|
|
230
|
+
private WebView webView;
|
|
231
|
+
|
|
232
|
+
public PrintInterface(Context context, WebView webView) {
|
|
233
|
+
this.context = context;
|
|
234
|
+
this.webView = webView;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@JavascriptInterface
|
|
238
|
+
public void print() {
|
|
239
|
+
// Run on UI thread since printing requires UI operations
|
|
240
|
+
((Activity) context).runOnUiThread(
|
|
241
|
+
new Runnable() {
|
|
242
|
+
@Override
|
|
243
|
+
public void run() {
|
|
244
|
+
// Create a print job from the WebView content
|
|
245
|
+
PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
|
|
246
|
+
String jobName = "Document_" + System.currentTimeMillis();
|
|
247
|
+
|
|
248
|
+
PrintDocumentAdapter printAdapter;
|
|
249
|
+
|
|
250
|
+
// For API 21+ (Lollipop and above)
|
|
251
|
+
printAdapter = webView.createPrintDocumentAdapter(jobName);
|
|
252
|
+
|
|
253
|
+
printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build());
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@SuppressLint({ "SetJavaScriptEnabled", "AddJavascriptInterface" })
|
|
261
|
+
public void presentWebView() {
|
|
262
|
+
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
263
|
+
setCancelable(true);
|
|
264
|
+
Objects.requireNonNull(getWindow()).setFlags(
|
|
265
|
+
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
266
|
+
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
267
|
+
);
|
|
268
|
+
setContentView(R.layout.activity_browser);
|
|
269
|
+
|
|
270
|
+
// If custom dimensions are set, configure for touch passthrough
|
|
271
|
+
if (_options != null && (_options.getWidth() != null || _options.getHeight() != null)) {
|
|
272
|
+
Window window = getWindow();
|
|
273
|
+
if (window != null) {
|
|
274
|
+
// Make the dialog background transparent
|
|
275
|
+
window.setBackgroundDrawableResource(android.R.color.transparent);
|
|
276
|
+
// Don't dim the background
|
|
277
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
|
278
|
+
// Allow touches outside to pass through
|
|
279
|
+
window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Set fitsSystemWindows only for Android 10 (API 29)
|
|
284
|
+
if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.Q) {
|
|
285
|
+
View coordinator = findViewById(R.id.coordinator_layout);
|
|
286
|
+
if (coordinator != null) coordinator.setFitsSystemWindows(true);
|
|
287
|
+
View appBar = findViewById(R.id.app_bar_layout);
|
|
288
|
+
if (appBar != null) appBar.setFitsSystemWindows(true);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
292
|
+
|
|
293
|
+
// Make status bar transparent
|
|
294
|
+
if (getWindow() != null) {
|
|
295
|
+
getWindow().setStatusBarColor(Color.TRANSPARENT);
|
|
296
|
+
|
|
297
|
+
// Add FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag to the window
|
|
298
|
+
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
299
|
+
|
|
300
|
+
// On Android 30+ clear FLAG_TRANSLUCENT_STATUS flag
|
|
301
|
+
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
WindowInsetsControllerCompat insetsController = new WindowInsetsControllerCompat(
|
|
305
|
+
getWindow(),
|
|
306
|
+
getWindow() != null ? getWindow().getDecorView() : null
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (getWindow() != null) {
|
|
310
|
+
getWindow()
|
|
311
|
+
.getDecorView()
|
|
312
|
+
.post(() -> {
|
|
313
|
+
// Get status bar height
|
|
314
|
+
int statusBarHeight = 0;
|
|
315
|
+
int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
|
|
316
|
+
if (resourceId > 0) {
|
|
317
|
+
statusBarHeight = getContext().getResources().getDimensionPixelSize(resourceId);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Find the status bar color view
|
|
321
|
+
View statusBarColorView = findViewById(R.id.status_bar_color_view);
|
|
322
|
+
|
|
323
|
+
// Set the height of the status bar color view
|
|
324
|
+
if (statusBarColorView != null) {
|
|
325
|
+
statusBarColorView.getLayoutParams().height = statusBarHeight;
|
|
326
|
+
statusBarColorView.requestLayout();
|
|
327
|
+
|
|
328
|
+
// Set color based on toolbar color or dark mode
|
|
329
|
+
if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
|
|
330
|
+
try {
|
|
331
|
+
// Use explicitly provided toolbar color for status bar
|
|
332
|
+
int toolbarColor = Color.parseColor(_options.getToolbarColor());
|
|
333
|
+
statusBarColorView.setBackgroundColor(toolbarColor);
|
|
334
|
+
|
|
335
|
+
// Set status bar text to white or black based on background
|
|
336
|
+
boolean isDarkBackground = isDarkColor(toolbarColor);
|
|
337
|
+
insetsController.setAppearanceLightStatusBars(!isDarkBackground);
|
|
338
|
+
} catch (IllegalArgumentException e) {
|
|
339
|
+
// Fallback to default black if color parsing fails
|
|
340
|
+
statusBarColorView.setBackgroundColor(Color.BLACK);
|
|
341
|
+
insetsController.setAppearanceLightStatusBars(false);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
// Follow system dark mode if no toolbar color provided
|
|
345
|
+
boolean isDarkTheme = isDarkThemeEnabled();
|
|
346
|
+
int statusBarColor = isDarkTheme ? Color.BLACK : Color.WHITE;
|
|
347
|
+
statusBarColorView.setBackgroundColor(statusBarColor);
|
|
348
|
+
insetsController.setAppearanceLightStatusBars(!isDarkTheme);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Set dimensions if specified, otherwise fullscreen
|
|
355
|
+
applyDimensions();
|
|
356
|
+
|
|
357
|
+
this._webView = findViewById(R.id.browser_view);
|
|
358
|
+
|
|
359
|
+
// Apply insets to fix edge-to-edge issues on Android 15+
|
|
360
|
+
applyInsets();
|
|
361
|
+
|
|
362
|
+
_webView.addJavascriptInterface(new JavaScriptInterface(), "AndroidInterface");
|
|
363
|
+
_webView.addJavascriptInterface(new PreShowScriptInterface(), "PreShowScriptInterface");
|
|
364
|
+
_webView.addJavascriptInterface(new PrintInterface(this._context, _webView), "PrintInterface");
|
|
365
|
+
_webView.getSettings().setJavaScriptEnabled(true);
|
|
366
|
+
_webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
|
|
367
|
+
_webView.getSettings().setDatabaseEnabled(true);
|
|
368
|
+
_webView.getSettings().setDomStorageEnabled(true);
|
|
369
|
+
_webView.getSettings().setAllowFileAccess(true);
|
|
370
|
+
_webView.getSettings().setLoadWithOverviewMode(true);
|
|
371
|
+
_webView.getSettings().setUseWideViewPort(true);
|
|
372
|
+
_webView.getSettings().setAllowFileAccessFromFileURLs(true);
|
|
373
|
+
_webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
|
|
374
|
+
_webView.getSettings().setMediaPlaybackRequiresUserGesture(false);
|
|
375
|
+
|
|
376
|
+
// Open links in external browser for target="_blank" if preventDeepLink is false
|
|
377
|
+
if (!_options.getPreventDeeplink()) {
|
|
378
|
+
_webView.getSettings().setSupportMultipleWindows(true);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Enhanced settings for Google Pay and Payment Request API support (only when enabled)
|
|
382
|
+
if (_options.getEnableGooglePaySupport()) {
|
|
383
|
+
Log.d("InAppBrowser", "Enabling Google Pay support features");
|
|
384
|
+
_webView.getSettings().setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
|
385
|
+
_webView.getSettings().setSupportMultipleWindows(true);
|
|
386
|
+
_webView.getSettings().setGeolocationEnabled(true);
|
|
387
|
+
|
|
388
|
+
// Ensure secure context for Payment Request API
|
|
389
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
390
|
+
_webView.getSettings().setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Enable Payment Request API only if feature is supported
|
|
394
|
+
if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
|
|
395
|
+
WebSettingsCompat.setPaymentRequestEnabled(_webView.getSettings(), true);
|
|
396
|
+
Log.d("InAppBrowser", "Payment Request API enabled");
|
|
397
|
+
} else {
|
|
398
|
+
Log.d("InAppBrowser", "Payment Request API not supported on this device");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Set web view background color
|
|
403
|
+
int backgroundColor = _options.getBackgroundColor().equals("white") ? Color.WHITE : Color.BLACK;
|
|
404
|
+
_webView.setBackgroundColor(backgroundColor);
|
|
405
|
+
|
|
406
|
+
// Set text zoom if specified in options
|
|
407
|
+
if (_options.getTextZoom() > 0) {
|
|
408
|
+
_webView.getSettings().setTextZoom(_options.getTextZoom());
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
_webView.setWebViewClient(new WebViewClient());
|
|
412
|
+
|
|
413
|
+
_webView.setWebChromeClient(
|
|
414
|
+
new WebChromeClient() {
|
|
415
|
+
// Enable file open dialog
|
|
416
|
+
@Override
|
|
417
|
+
public boolean onShowFileChooser(
|
|
418
|
+
WebView webView,
|
|
419
|
+
ValueCallback<Uri[]> filePathCallback,
|
|
420
|
+
FileChooserParams fileChooserParams
|
|
421
|
+
) {
|
|
422
|
+
// Get the accept type safely
|
|
423
|
+
String acceptType;
|
|
424
|
+
if (
|
|
425
|
+
fileChooserParams.getAcceptTypes() != null &&
|
|
426
|
+
fileChooserParams.getAcceptTypes().length > 0 &&
|
|
427
|
+
!TextUtils.isEmpty(fileChooserParams.getAcceptTypes()[0])
|
|
428
|
+
) {
|
|
429
|
+
acceptType = fileChooserParams.getAcceptTypes()[0];
|
|
430
|
+
} else {
|
|
431
|
+
acceptType = "*/*";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// DEBUG: Log details about the file chooser request
|
|
435
|
+
Log.d("InAppBrowser", "onShowFileChooser called");
|
|
436
|
+
Log.d("InAppBrowser", "Accept type: " + acceptType);
|
|
437
|
+
Log.d("InAppBrowser", "Current URL: " + getUrl());
|
|
438
|
+
Log.d("InAppBrowser", "Original URL: " + (webView.getOriginalUrl() != null ? webView.getOriginalUrl() : "null"));
|
|
439
|
+
Log.d(
|
|
440
|
+
"InAppBrowser",
|
|
441
|
+
"Has camera permission: " +
|
|
442
|
+
(activity != null &&
|
|
443
|
+
activity.checkSelfPermission(android.Manifest.permission.CAMERA) ==
|
|
444
|
+
android.content.pm.PackageManager.PERMISSION_GRANTED)
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// Check if the file chooser is already open
|
|
448
|
+
if (mFilePathCallback != null) {
|
|
449
|
+
mFilePathCallback.onReceiveValue(null);
|
|
450
|
+
mFilePathCallback = null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
mFilePathCallback = filePathCallback;
|
|
454
|
+
|
|
455
|
+
// Direct check for capture attribute in URL (fallback method)
|
|
456
|
+
boolean isCaptureInUrl;
|
|
457
|
+
String captureMode;
|
|
458
|
+
String currentUrl = getUrl();
|
|
459
|
+
|
|
460
|
+
// Look for capture in URL parameters - sometimes the attribute shows up in URL
|
|
461
|
+
if (currentUrl != null && currentUrl.contains("capture=")) {
|
|
462
|
+
isCaptureInUrl = true;
|
|
463
|
+
captureMode = currentUrl.contains("capture=user") ? "user" : "environment";
|
|
464
|
+
Log.d("InAppBrowser", "Found capture in URL: " + captureMode);
|
|
465
|
+
} else {
|
|
466
|
+
captureMode = null;
|
|
467
|
+
isCaptureInUrl = false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// For image inputs, try to detect capture attribute using JavaScript
|
|
471
|
+
if (acceptType.equals("image/*")) {
|
|
472
|
+
// Check if HTML content contains capture attribute on file inputs (synchronous check)
|
|
473
|
+
webView.evaluateJavascript(
|
|
474
|
+
"document.querySelector('input[type=\"file\"][capture]') !== null",
|
|
475
|
+
(hasCaptureValue) -> {
|
|
476
|
+
Log.d("InAppBrowser", "Quick capture check: " + hasCaptureValue);
|
|
477
|
+
if (Boolean.parseBoolean(hasCaptureValue.replace("\"", ""))) {
|
|
478
|
+
Log.d("InAppBrowser", "Found capture attribute in quick check");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// Fixed JavaScript with proper error handling
|
|
484
|
+
String js = """
|
|
485
|
+
try {
|
|
486
|
+
(function() {
|
|
487
|
+
var captureAttr = null;
|
|
488
|
+
// Check active element first
|
|
489
|
+
if (document.activeElement &&
|
|
490
|
+
document.activeElement.tagName === 'INPUT' &&
|
|
491
|
+
document.activeElement.type === 'file') {
|
|
492
|
+
if (document.activeElement.hasAttribute('capture')) {
|
|
493
|
+
captureAttr = document.activeElement.getAttribute('capture') || 'environment';
|
|
494
|
+
return captureAttr;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Try to find any input with capture attribute
|
|
498
|
+
var inputs = document.querySelectorAll('input[type="file"][capture]');
|
|
499
|
+
if (inputs && inputs.length > 0) {
|
|
500
|
+
captureAttr = inputs[0].getAttribute('capture') || 'environment';
|
|
501
|
+
return captureAttr;
|
|
502
|
+
}
|
|
503
|
+
// Try to extract from HTML attributes
|
|
504
|
+
var allInputs = document.getElementsByTagName('input');
|
|
505
|
+
for (var i = 0; i < allInputs.length; i++) {
|
|
506
|
+
var input = allInputs[i];
|
|
507
|
+
if (input.type === 'file') {
|
|
508
|
+
if (input.hasAttribute('capture')) {
|
|
509
|
+
captureAttr = input.getAttribute('capture') || 'environment';
|
|
510
|
+
return captureAttr;
|
|
511
|
+
}
|
|
512
|
+
// Look for the accept attribute containing image/* as this might be a camera input
|
|
513
|
+
var acceptAttr = input.getAttribute('accept');
|
|
514
|
+
if (acceptAttr && acceptAttr.indexOf('image/*') >= 0) {
|
|
515
|
+
console.log('Found input with image/* accept');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return '';
|
|
520
|
+
})();
|
|
521
|
+
} catch(e) {
|
|
522
|
+
console.error('Capture detection error:', e);
|
|
523
|
+
return '';
|
|
524
|
+
}
|
|
525
|
+
""";
|
|
526
|
+
|
|
527
|
+
webView.evaluateJavascript(js, (value) -> {
|
|
528
|
+
Log.d("InAppBrowser", "Capture attribute JS result: " + value);
|
|
529
|
+
|
|
530
|
+
// If we already found capture in URL, use that directly
|
|
531
|
+
if (isCaptureInUrl) {
|
|
532
|
+
Log.d("InAppBrowser", "Using capture from URL: " + captureMode);
|
|
533
|
+
launchCamera(captureMode.equals("user"));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Process JavaScript result
|
|
538
|
+
if (value != null && value.length() > 2) {
|
|
539
|
+
// Clean up the value (remove quotes)
|
|
540
|
+
String captureValue = value.replace("\"", "");
|
|
541
|
+
Log.d("InAppBrowser", "Found capture attribute: " + captureValue);
|
|
542
|
+
|
|
543
|
+
if (!captureValue.isEmpty()) {
|
|
544
|
+
activity.runOnUiThread(() -> launchCamera(captureValue.equals("user")));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Look for hints in the web page source
|
|
550
|
+
Log.d("InAppBrowser", "Looking for camera hints in page content");
|
|
551
|
+
webView.evaluateJavascript("(function() { return document.documentElement.innerHTML; })()", (htmlSource) -> {
|
|
552
|
+
if (htmlSource != null && htmlSource.length() > 10) {
|
|
553
|
+
boolean hasCameraOrSelfieKeyword =
|
|
554
|
+
htmlSource.contains("capture=") || htmlSource.contains("camera") || htmlSource.contains("selfie");
|
|
555
|
+
|
|
556
|
+
Log.d("InAppBrowser", "Page contains camera keywords: " + hasCameraOrSelfieKeyword);
|
|
557
|
+
|
|
558
|
+
if (
|
|
559
|
+
hasCameraOrSelfieKeyword &&
|
|
560
|
+
currentUrl != null &&
|
|
561
|
+
(currentUrl.contains("selfie") || currentUrl.contains("camera") || currentUrl.contains("photo"))
|
|
562
|
+
) {
|
|
563
|
+
Log.d("InAppBrowser", "URL suggests camera usage, launching camera");
|
|
564
|
+
activity.runOnUiThread(() -> launchCamera(currentUrl.contains("selfie")));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// If all detection methods fail, fall back to regular file picker
|
|
570
|
+
Log.d("InAppBrowser", "No capture attribute detected, using file picker");
|
|
571
|
+
openFileChooser(
|
|
572
|
+
filePathCallback,
|
|
573
|
+
acceptType,
|
|
574
|
+
fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE
|
|
575
|
+
);
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// For non-image types, use regular file picker
|
|
582
|
+
openFileChooser(filePathCallback, acceptType, fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE);
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Launch the camera app for capturing images
|
|
588
|
+
* @param useFrontCamera true to use front camera, false for back camera
|
|
589
|
+
*/
|
|
590
|
+
private void launchCamera(boolean useFrontCamera) {
|
|
591
|
+
Log.d("InAppBrowser", "Launching camera, front camera: " + useFrontCamera);
|
|
592
|
+
|
|
593
|
+
// First check if we have camera permission
|
|
594
|
+
if (activity != null && permissionHandler != null) {
|
|
595
|
+
// Create a temporary permission request to check camera permission
|
|
596
|
+
android.webkit.PermissionRequest tempRequest = new android.webkit.PermissionRequest() {
|
|
597
|
+
@Override
|
|
598
|
+
public Uri getOrigin() {
|
|
599
|
+
return Uri.parse("file:///android_asset/");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
@Override
|
|
603
|
+
public String[] getResources() {
|
|
604
|
+
return new String[] { PermissionRequest.RESOURCE_VIDEO_CAPTURE };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
@Override
|
|
608
|
+
public void grant(String[] resources) {
|
|
609
|
+
// Permission granted, now launch the camera
|
|
610
|
+
launchCameraWithPermission(useFrontCamera);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@Override
|
|
614
|
+
public void deny() {
|
|
615
|
+
// Permission denied, fall back to file picker
|
|
616
|
+
Log.e("InAppBrowser", "Camera permission denied, falling back to file picker");
|
|
617
|
+
fallbackToFilePicker();
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// Request camera permission through the plugin
|
|
622
|
+
permissionHandler.handleCameraPermissionRequest(tempRequest);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// If we can't request permission, try launching directly
|
|
627
|
+
launchCameraWithPermission(useFrontCamera);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Launch camera after permission is granted
|
|
632
|
+
*/
|
|
633
|
+
private void launchCameraWithPermission(boolean useFrontCamera) {
|
|
634
|
+
try {
|
|
635
|
+
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
|
636
|
+
if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
|
637
|
+
File photoFile = null;
|
|
638
|
+
try {
|
|
639
|
+
photoFile = createImageFile();
|
|
640
|
+
} catch (IOException ex) {
|
|
641
|
+
Log.e("InAppBrowser", "Error creating image file", ex);
|
|
642
|
+
fallbackToFilePicker();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (photoFile != null) {
|
|
647
|
+
tempCameraUri = FileProvider.getUriForFile(
|
|
648
|
+
activity,
|
|
649
|
+
activity.getPackageName() + ".fileprovider",
|
|
650
|
+
photoFile
|
|
651
|
+
);
|
|
652
|
+
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, tempCameraUri);
|
|
653
|
+
|
|
654
|
+
if (useFrontCamera) {
|
|
655
|
+
takePictureIntent.putExtra("android.intent.extras.CAMERA_FACING", 1);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
if (activity instanceof androidx.activity.ComponentActivity) {
|
|
660
|
+
androidx.activity.ComponentActivity componentActivity =
|
|
661
|
+
(androidx.activity.ComponentActivity) activity;
|
|
662
|
+
componentActivity
|
|
663
|
+
.getActivityResultRegistry()
|
|
664
|
+
.register(
|
|
665
|
+
"camera_capture",
|
|
666
|
+
new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
|
|
667
|
+
(result) -> {
|
|
668
|
+
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
669
|
+
if (tempCameraUri != null) {
|
|
670
|
+
mFilePathCallback.onReceiveValue(new Uri[] { tempCameraUri });
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
mFilePathCallback.onReceiveValue(null);
|
|
674
|
+
}
|
|
675
|
+
mFilePathCallback = null;
|
|
676
|
+
tempCameraUri = null;
|
|
677
|
+
}
|
|
678
|
+
)
|
|
679
|
+
.launch(takePictureIntent);
|
|
680
|
+
} else {
|
|
681
|
+
// Fallback for non-ComponentActivity
|
|
682
|
+
activity.startActivityForResult(takePictureIntent, FILE_CHOOSER_REQUEST_CODE);
|
|
683
|
+
}
|
|
684
|
+
} catch (SecurityException e) {
|
|
685
|
+
Log.e("InAppBrowser", "Security exception launching camera: " + e.getMessage(), e);
|
|
686
|
+
fallbackToFilePicker();
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
Log.e("InAppBrowser", "Failed to create photo URI, falling back to file picker");
|
|
690
|
+
fallbackToFilePicker();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} catch (Exception e) {
|
|
694
|
+
Log.e("InAppBrowser", "Camera launch failed: " + e.getMessage(), e);
|
|
695
|
+
fallbackToFilePicker();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Fall back to file picker when camera launch fails
|
|
701
|
+
*/
|
|
702
|
+
private void fallbackToFilePicker() {
|
|
703
|
+
if (mFilePathCallback != null) {
|
|
704
|
+
openFileChooser(mFilePathCallback, "image/*", false);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Grant permissions for cam
|
|
709
|
+
@Override
|
|
710
|
+
public void onPermissionRequest(final PermissionRequest request) {
|
|
711
|
+
Log.i("INAPPBROWSER", "onPermissionRequest " + Arrays.toString(request.getResources()));
|
|
712
|
+
final String[] requestedResources = request.getResources();
|
|
713
|
+
for (String r : requestedResources) {
|
|
714
|
+
Log.i("INAPPBROWSER", "requestedResources " + r);
|
|
715
|
+
if (r.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
|
|
716
|
+
Log.i("INAPPBROWSER", "RESOURCE_VIDEO_CAPTURE req");
|
|
717
|
+
// Store the permission request
|
|
718
|
+
currentPermissionRequest = request;
|
|
719
|
+
// Initiate the permission request through the plugin
|
|
720
|
+
if (permissionHandler != null) {
|
|
721
|
+
permissionHandler.handleCameraPermissionRequest(request);
|
|
722
|
+
}
|
|
723
|
+
return; // Return here to avoid denying the request
|
|
724
|
+
} else if (r.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
|
|
725
|
+
Log.i("INAPPBROWSER", "RESOURCE_AUDIO_CAPTURE req");
|
|
726
|
+
// Store the permission request
|
|
727
|
+
currentPermissionRequest = request;
|
|
728
|
+
// Initiate the permission request through the plugin
|
|
729
|
+
if (permissionHandler != null) {
|
|
730
|
+
permissionHandler.handleMicrophonePermissionRequest(request);
|
|
731
|
+
}
|
|
732
|
+
return; // Return here to avoid denying the request
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
// If no matching permission is found, deny the request
|
|
736
|
+
request.deny();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
@Override
|
|
740
|
+
public void onPermissionRequestCanceled(PermissionRequest request) {
|
|
741
|
+
super.onPermissionRequestCanceled(request);
|
|
742
|
+
Toast.makeText(WebViewDialog.this.activity, "Permission Denied", Toast.LENGTH_SHORT).show();
|
|
743
|
+
// Handle the denied permission
|
|
744
|
+
if (currentPermissionRequest != null) {
|
|
745
|
+
currentPermissionRequest.deny();
|
|
746
|
+
currentPermissionRequest = null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Handle geolocation permission requests
|
|
751
|
+
@Override
|
|
752
|
+
public void onGeolocationPermissionsShowPrompt(String origin, android.webkit.GeolocationPermissions.Callback callback) {
|
|
753
|
+
Log.i("INAPPBROWSER", "onGeolocationPermissionsShowPrompt for origin: " + origin);
|
|
754
|
+
// Grant geolocation permission automatically for openWebView
|
|
755
|
+
// This allows websites to access location when opened with openWebView
|
|
756
|
+
callback.invoke(origin, true, false);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// This method will be called at page load, a good place to inject customizations
|
|
760
|
+
@Override
|
|
761
|
+
public void onProgressChanged(WebView view, int newProgress) {
|
|
762
|
+
super.onProgressChanged(view, newProgress);
|
|
763
|
+
|
|
764
|
+
// When the page is almost loaded, inject our date picker customization
|
|
765
|
+
// Only if materialPicker option is enabled
|
|
766
|
+
if (newProgress > 75 && !datePickerInjected && _options.getMaterialPicker()) {
|
|
767
|
+
injectDatePickerFixes();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Support for Google Pay and popup windows (critical for OR_BIBED_15 fix)
|
|
772
|
+
@Override
|
|
773
|
+
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, android.os.Message resultMsg) {
|
|
774
|
+
Log.d(
|
|
775
|
+
"InAppBrowser",
|
|
776
|
+
"onCreateWindow called - isUserGesture: " +
|
|
777
|
+
isUserGesture +
|
|
778
|
+
", GooglePaySupport: " +
|
|
779
|
+
_options.getEnableGooglePaySupport() +
|
|
780
|
+
", preventDeeplink: " +
|
|
781
|
+
_options.getPreventDeeplink()
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
// When preventDeeplink is false, open target="_blank" links externally
|
|
785
|
+
if (!_options.getPreventDeeplink() && isUserGesture) {
|
|
786
|
+
try {
|
|
787
|
+
WebView.HitTestResult result = view.getHitTestResult();
|
|
788
|
+
String data = result.getExtra();
|
|
789
|
+
if (data != null && !data.isEmpty()) {
|
|
790
|
+
Log.d("InAppBrowser", "Opening target=_blank link externally: " + data);
|
|
791
|
+
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(data));
|
|
792
|
+
browserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
793
|
+
_webView.getContext().startActivity(browserIntent);
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
} catch (Exception e) {
|
|
797
|
+
Log.e("InAppBrowser", "Error opening external link: " + e.getMessage());
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Only handle popup windows if Google Pay support is enabled
|
|
802
|
+
if (_options.getEnableGooglePaySupport() && isUserGesture) {
|
|
803
|
+
// Create a new WebView for the popup
|
|
804
|
+
WebView popupWebView = new WebView(activity);
|
|
805
|
+
popupWebView.getSettings().setJavaScriptEnabled(true);
|
|
806
|
+
popupWebView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
|
|
807
|
+
popupWebView.getSettings().setSupportMultipleWindows(true);
|
|
808
|
+
|
|
809
|
+
// Set WebViewClient to handle URL loading and closing
|
|
810
|
+
popupWebView.setWebViewClient(
|
|
811
|
+
new WebViewClient() {
|
|
812
|
+
@Override
|
|
813
|
+
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
|
814
|
+
Log.d("InAppBrowser", "Popup WebView loading URL: " + url);
|
|
815
|
+
|
|
816
|
+
// Handle Google Pay result URLs or close conditions
|
|
817
|
+
if (url.contains("google.com/pay") || url.contains("close") || url.contains("cancel")) {
|
|
818
|
+
Log.d("InAppBrowser", "Closing popup for Google Pay result");
|
|
819
|
+
// Notify the parent WebView and close popup
|
|
820
|
+
activity.runOnUiThread(() -> {
|
|
821
|
+
try {
|
|
822
|
+
if (popupWebView.getParent() != null) {
|
|
823
|
+
((ViewGroup) popupWebView.getParent()).removeView(popupWebView);
|
|
824
|
+
}
|
|
825
|
+
popupWebView.destroy();
|
|
826
|
+
} catch (Exception e) {
|
|
827
|
+
Log.e("InAppBrowser", "Error closing popup: " + e.getMessage());
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
// Set up the popup WebView transport
|
|
838
|
+
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
|
|
839
|
+
transport.setWebView(popupWebView);
|
|
840
|
+
resultMsg.sendToTarget();
|
|
841
|
+
|
|
842
|
+
Log.d("InAppBrowser", "Created popup window for Google Pay");
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
Map<String, String> requestHeaders = new HashMap<>();
|
|
852
|
+
if (_options.getHeaders() != null) {
|
|
853
|
+
Iterator<String> keys = _options.getHeaders().keys();
|
|
854
|
+
while (keys.hasNext()) {
|
|
855
|
+
String key = keys.next();
|
|
856
|
+
if (TextUtils.equals(key.toLowerCase(), "user-agent")) {
|
|
857
|
+
_webView.getSettings().setUserAgentString(_options.getHeaders().getString(key));
|
|
858
|
+
} else {
|
|
859
|
+
requestHeaders.put(key, _options.getHeaders().getString(key));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
_webView.loadUrl(this._options.getUrl(), requestHeaders);
|
|
865
|
+
_webView.requestFocus();
|
|
866
|
+
_webView.requestFocusFromTouch();
|
|
867
|
+
|
|
868
|
+
// Inject JavaScript interface early to ensure it's available immediately
|
|
869
|
+
// This complements the injection in onPageFinished and doUpdateVisitedHistory
|
|
870
|
+
_webView.post(() -> {
|
|
871
|
+
if (_webView != null) {
|
|
872
|
+
injectJavaScriptInterface();
|
|
873
|
+
|
|
874
|
+
// Inject Google Pay support enhancements if enabled
|
|
875
|
+
if (_options.getEnableGooglePaySupport()) {
|
|
876
|
+
injectGooglePayPolyfills();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
Log.d("InAppBrowser", "JavaScript interface injected early after URL load");
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
setupToolbar();
|
|
884
|
+
setWebViewClient();
|
|
885
|
+
|
|
886
|
+
if (!this._options.isPresentAfterPageLoad()) {
|
|
887
|
+
show();
|
|
888
|
+
_options.getPluginCall().resolve();
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Apply window insets to the WebView to properly handle edge-to-edge display
|
|
894
|
+
* and fix status bar overlap issues on Android 15+
|
|
895
|
+
*/
|
|
896
|
+
private void applyInsets() {
|
|
897
|
+
if (_webView == null) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Check if we need Android 15+ specific fixes
|
|
902
|
+
boolean isAndroid15Plus = Build.VERSION.SDK_INT >= 35;
|
|
903
|
+
|
|
904
|
+
// Get parent view
|
|
905
|
+
ViewGroup parent = (ViewGroup) _webView.getParent();
|
|
906
|
+
|
|
907
|
+
// Find status bar color view and toolbar for Android 15+ specific handling
|
|
908
|
+
View statusBarColorView = findViewById(R.id.status_bar_color_view);
|
|
909
|
+
View toolbarView = findViewById(R.id.tool_bar);
|
|
910
|
+
|
|
911
|
+
// Special handling for Android 15+
|
|
912
|
+
if (isAndroid15Plus) {
|
|
913
|
+
// Get AppBarLayout which contains the toolbar
|
|
914
|
+
if (toolbarView != null && toolbarView.getParent() instanceof com.google.android.material.appbar.AppBarLayout appBarLayout) {
|
|
915
|
+
// Remove elevation to eliminate shadows (only on Android 15+)
|
|
916
|
+
appBarLayout.setElevation(0);
|
|
917
|
+
appBarLayout.setStateListAnimator(null);
|
|
918
|
+
appBarLayout.setOutlineProvider(null);
|
|
919
|
+
|
|
920
|
+
// Determine background color to use
|
|
921
|
+
int backgroundColor = Color.BLACK; // Default fallback
|
|
922
|
+
if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
|
|
923
|
+
try {
|
|
924
|
+
backgroundColor = Color.parseColor(_options.getToolbarColor());
|
|
925
|
+
} catch (IllegalArgumentException e) {
|
|
926
|
+
Log.e("InAppBrowser", "Invalid toolbar color, using black: " + e.getMessage());
|
|
927
|
+
}
|
|
928
|
+
} else {
|
|
929
|
+
// Follow system theme if no color specified
|
|
930
|
+
boolean isDarkTheme = isDarkThemeEnabled();
|
|
931
|
+
backgroundColor = isDarkTheme ? Color.BLACK : Color.WHITE;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Apply fixes for Android 15+ using a delayed post
|
|
935
|
+
final int finalBgColor = backgroundColor;
|
|
936
|
+
_webView.post(() -> {
|
|
937
|
+
// Get status bar height
|
|
938
|
+
int statusBarHeight = 0;
|
|
939
|
+
int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
|
|
940
|
+
if (resourceId > 0) {
|
|
941
|
+
statusBarHeight = getContext().getResources().getDimensionPixelSize(resourceId);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Fix status bar view
|
|
945
|
+
if (statusBarColorView != null) {
|
|
946
|
+
ViewGroup.LayoutParams params = statusBarColorView.getLayoutParams();
|
|
947
|
+
params.height = statusBarHeight;
|
|
948
|
+
statusBarColorView.setLayoutParams(params);
|
|
949
|
+
statusBarColorView.setBackgroundColor(finalBgColor);
|
|
950
|
+
statusBarColorView.setVisibility(View.VISIBLE);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Fix AppBarLayout position
|
|
954
|
+
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) appBarLayout.getLayoutParams();
|
|
955
|
+
params.topMargin = statusBarHeight;
|
|
956
|
+
appBarLayout.setLayoutParams(params);
|
|
957
|
+
appBarLayout.setBackgroundColor(finalBgColor);
|
|
958
|
+
View contentBrowserLayout = findViewById(R.id.content_browser_layout);
|
|
959
|
+
View parentContainer = findViewById(android.R.id.content);
|
|
960
|
+
if (contentBrowserLayout == null || parentContainer == null) {
|
|
961
|
+
Log.w("InAppBrowser", "Required views not found for height calculation");
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
ViewGroup.LayoutParams layoutParams = contentBrowserLayout.getLayoutParams();
|
|
966
|
+
if (!(layoutParams instanceof ViewGroup.MarginLayoutParams)) {
|
|
967
|
+
Log.w("InAppBrowser", "Content browser layout does not support margins");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
ViewGroup.MarginLayoutParams mlpContentBrowserLayout = (ViewGroup.MarginLayoutParams) layoutParams;
|
|
971
|
+
|
|
972
|
+
int parentHeight = parentContainer.getHeight();
|
|
973
|
+
int appBarHeight = appBarLayout.getHeight(); // can be 0 if not visible with the toolbar type BLANK
|
|
974
|
+
|
|
975
|
+
if (parentHeight <= 0) {
|
|
976
|
+
Log.w("InAppBrowser", "Parent dimensions not yet available");
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Recompute the height of the content browser to be able to set margin bottom as we want to
|
|
981
|
+
mlpContentBrowserLayout.height = parentHeight - (statusBarHeight + appBarHeight);
|
|
982
|
+
contentBrowserLayout.setLayoutParams(mlpContentBrowserLayout);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Apply system insets to WebView content view (compatible with all Android versions)
|
|
988
|
+
ViewCompat.setOnApplyWindowInsetsListener(_webView, (v, windowInsets) -> {
|
|
989
|
+
Insets bars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
|
990
|
+
Insets ime = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
|
|
991
|
+
Boolean keyboardVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime());
|
|
992
|
+
|
|
993
|
+
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
|
|
994
|
+
|
|
995
|
+
// Apply safe margin inset to bottom margin if enabled in options or fallback to 0px
|
|
996
|
+
int navBottom = _options.getEnabledSafeMargin() ? bars.bottom : 0;
|
|
997
|
+
|
|
998
|
+
// Apply top inset only if useTopInset option is enabled or fallback to 0px
|
|
999
|
+
int navTop = _options.getUseTopInset() ? bars.top : 0;
|
|
1000
|
+
|
|
1001
|
+
// Avoid double-applying top inset; AppBar/status bar handled above on Android 15+
|
|
1002
|
+
mlp.topMargin = isAndroid15Plus ? 0 : navTop;
|
|
1003
|
+
|
|
1004
|
+
// Apply larger of navigation bar or keyboard inset to bottom margin
|
|
1005
|
+
mlp.bottomMargin = Math.max(navBottom, ime.bottom);
|
|
1006
|
+
|
|
1007
|
+
mlp.leftMargin = bars.left;
|
|
1008
|
+
mlp.rightMargin = bars.right;
|
|
1009
|
+
v.setLayoutParams(mlp);
|
|
1010
|
+
|
|
1011
|
+
return WindowInsetsCompat.CONSUMED;
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// Handle window decoration - version-specific handling
|
|
1015
|
+
if (getWindow() != null) {
|
|
1016
|
+
if (isAndroid15Plus) {
|
|
1017
|
+
// Android 15+: Use edge-to-edge with proper insets handling
|
|
1018
|
+
getWindow().setDecorFitsSystemWindows(false);
|
|
1019
|
+
getWindow().setStatusBarColor(Color.TRANSPARENT);
|
|
1020
|
+
getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
|
1021
|
+
|
|
1022
|
+
WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView());
|
|
1023
|
+
|
|
1024
|
+
// Set status bar text color
|
|
1025
|
+
if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
|
|
1026
|
+
try {
|
|
1027
|
+
int backgroundColor = Color.parseColor(_options.getToolbarColor());
|
|
1028
|
+
boolean isDarkBackground = isDarkColor(backgroundColor);
|
|
1029
|
+
controller.setAppearanceLightStatusBars(!isDarkBackground);
|
|
1030
|
+
} catch (IllegalArgumentException e) {
|
|
1031
|
+
// Ignore color parsing errors
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
} else if (Build.VERSION.SDK_INT >= 30) {
|
|
1035
|
+
// Android 11-14: Keep navigation bar transparent but respect status bar
|
|
1036
|
+
getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
|
1037
|
+
|
|
1038
|
+
WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView());
|
|
1039
|
+
|
|
1040
|
+
// Set status bar color to match toolbar or use system default
|
|
1041
|
+
if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
|
|
1042
|
+
try {
|
|
1043
|
+
int toolbarColor = Color.parseColor(_options.getToolbarColor());
|
|
1044
|
+
getWindow().setStatusBarColor(toolbarColor);
|
|
1045
|
+
boolean isDarkBackground = isDarkColor(toolbarColor);
|
|
1046
|
+
controller.setAppearanceLightStatusBars(!isDarkBackground);
|
|
1047
|
+
} catch (IllegalArgumentException e) {
|
|
1048
|
+
// Follow system theme if color parsing fails
|
|
1049
|
+
boolean isDarkTheme = isDarkThemeEnabled();
|
|
1050
|
+
int statusBarColor = isDarkTheme ? Color.BLACK : Color.WHITE;
|
|
1051
|
+
getWindow().setStatusBarColor(statusBarColor);
|
|
1052
|
+
controller.setAppearanceLightStatusBars(!isDarkTheme);
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
// Follow system theme if no toolbar color provided
|
|
1056
|
+
boolean isDarkTheme = isDarkThemeEnabled();
|
|
1057
|
+
int statusBarColor = isDarkTheme ? Color.BLACK : Color.WHITE;
|
|
1058
|
+
getWindow().setStatusBarColor(statusBarColor);
|
|
1059
|
+
controller.setAppearanceLightStatusBars(!isDarkTheme);
|
|
1060
|
+
}
|
|
1061
|
+
} else {
|
|
1062
|
+
// Pre-Android 11: Use deprecated flags for edge-to-edge navigation bar only
|
|
1063
|
+
getWindow()
|
|
1064
|
+
.getDecorView()
|
|
1065
|
+
.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
|
|
1066
|
+
|
|
1067
|
+
getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
|
1068
|
+
|
|
1069
|
+
// Set status bar color to match toolbar
|
|
1070
|
+
if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
|
|
1071
|
+
try {
|
|
1072
|
+
int toolbarColor = Color.parseColor(_options.getToolbarColor());
|
|
1073
|
+
getWindow().setStatusBarColor(toolbarColor);
|
|
1074
|
+
} catch (IllegalArgumentException e) {
|
|
1075
|
+
// Use system default
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
public void postMessageToJS(Object detail) {
|
|
1083
|
+
if (_webView != null) {
|
|
1084
|
+
try {
|
|
1085
|
+
JSONObject jsonObject = new JSONObject();
|
|
1086
|
+
jsonObject.put("detail", detail);
|
|
1087
|
+
String jsonDetail = jsonObject.toString();
|
|
1088
|
+
String script = String.format("window.dispatchEvent(new CustomEvent('messageFromNative', %s));", jsonDetail);
|
|
1089
|
+
_webView.post(() -> {
|
|
1090
|
+
if (_webView != null) {
|
|
1091
|
+
_webView.evaluateJavascript(script, null);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
} catch (Exception e) {
|
|
1095
|
+
Log.e("postMessageToJS", "Error sending message to JS: " + e.getMessage());
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private void injectJavaScriptInterface() {
|
|
1101
|
+
if (_webView == null) {
|
|
1102
|
+
Log.w("InAppBrowser", "Cannot inject JavaScript interface - WebView is null");
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
try {
|
|
1107
|
+
String script = """
|
|
1108
|
+
(function() {
|
|
1109
|
+
if (window.AndroidInterface) {
|
|
1110
|
+
// Create mobileApp object for backward compatibility
|
|
1111
|
+
if (!window.mobileApp) {
|
|
1112
|
+
window.mobileApp = {
|
|
1113
|
+
postMessage: function(message) {
|
|
1114
|
+
try {
|
|
1115
|
+
var msg = typeof message === 'string' ? message : JSON.stringify(message);
|
|
1116
|
+
window.AndroidInterface.postMessage(msg);
|
|
1117
|
+
} catch(e) {
|
|
1118
|
+
console.error('Error in mobileApp.postMessage:', e);
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
close: function() {
|
|
1122
|
+
try {
|
|
1123
|
+
window.AndroidInterface.close();
|
|
1124
|
+
} catch(e) {
|
|
1125
|
+
console.error('Error in mobileApp.close:', e);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// Override window.print function to use our PrintInterface
|
|
1132
|
+
if (window.PrintInterface) {
|
|
1133
|
+
window.print = function() {
|
|
1134
|
+
try {
|
|
1135
|
+
window.PrintInterface.print();
|
|
1136
|
+
} catch(e) {
|
|
1137
|
+
console.error('Error in print:', e);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
})();
|
|
1142
|
+
""";
|
|
1143
|
+
|
|
1144
|
+
_webView.post(() -> {
|
|
1145
|
+
if (_webView != null) {
|
|
1146
|
+
try {
|
|
1147
|
+
_webView.evaluateJavascript(script, null);
|
|
1148
|
+
} catch (Exception e) {
|
|
1149
|
+
Log.e("InAppBrowser", "Error injecting JavaScript interface: " + e.getMessage());
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
} catch (Exception e) {
|
|
1154
|
+
Log.e("InAppBrowser", "Error preparing JavaScript interface: " + e.getMessage());
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Injects JavaScript polyfills and enhancements for Google Pay support
|
|
1160
|
+
* Helps resolve OR_BIBED_15 errors by ensuring proper cross-origin handling
|
|
1161
|
+
*/
|
|
1162
|
+
private void injectGooglePayPolyfills() {
|
|
1163
|
+
if (_webView == null) {
|
|
1164
|
+
Log.w("InAppBrowser", "Cannot inject Google Pay polyfills - WebView is null");
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
try {
|
|
1169
|
+
String googlePayScript = """
|
|
1170
|
+
(function() {
|
|
1171
|
+
console.log('[InAppBrowser] Injecting Google Pay support enhancements');
|
|
1172
|
+
|
|
1173
|
+
// Enhance window.open to work better with Google Pay popups
|
|
1174
|
+
const originalWindowOpen = window.open;
|
|
1175
|
+
window.open = function(url, target, features) {
|
|
1176
|
+
console.log('[InAppBrowser] Enhanced window.open called:', url, target, features);
|
|
1177
|
+
|
|
1178
|
+
// For Google Pay URLs, ensure they open in a new context
|
|
1179
|
+
if (url && (url.includes('google.com/pay') || url.includes('accounts.google.com'))) {
|
|
1180
|
+
console.log('[InAppBrowser] Google Pay popup detected, using enhanced handling');
|
|
1181
|
+
// Let the native WebView handle this via onCreateWindow
|
|
1182
|
+
return originalWindowOpen.call(window, url, '_blank', features);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return originalWindowOpen.call(window, url, target, features);
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
// Ensure proper Payment Request API context
|
|
1189
|
+
if (window.PaymentRequest) {
|
|
1190
|
+
console.log('[InAppBrowser] Payment Request API available');
|
|
1191
|
+
|
|
1192
|
+
// Wrap PaymentRequest constructor to add better error handling
|
|
1193
|
+
const OriginalPaymentRequest = window.PaymentRequest;
|
|
1194
|
+
window.PaymentRequest = function(methodData, details, options) {
|
|
1195
|
+
console.log('[InAppBrowser] PaymentRequest created with enhanced error handling');
|
|
1196
|
+
const request = new OriginalPaymentRequest(methodData, details, options);
|
|
1197
|
+
|
|
1198
|
+
// Override show method to handle popup blocking issues
|
|
1199
|
+
const originalShow = request.show;
|
|
1200
|
+
request.show = function() {
|
|
1201
|
+
console.log('[InAppBrowser] PaymentRequest.show() called');
|
|
1202
|
+
return originalShow.call(this).catch((error) => {
|
|
1203
|
+
console.error('[InAppBrowser] PaymentRequest error:', error);
|
|
1204
|
+
if (error.name === 'SecurityError' || error.message.includes('popup')) {
|
|
1205
|
+
console.log('[InAppBrowser] Attempting to handle popup blocking issue');
|
|
1206
|
+
}
|
|
1207
|
+
throw error;
|
|
1208
|
+
});
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
return request;
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
// Copy static methods
|
|
1215
|
+
Object.setPrototypeOf(window.PaymentRequest, OriginalPaymentRequest);
|
|
1216
|
+
Object.defineProperty(window.PaymentRequest, 'prototype', {
|
|
1217
|
+
value: OriginalPaymentRequest.prototype
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Add meta tag to ensure proper cross-origin handling if not present
|
|
1222
|
+
if (!document.querySelector('meta[http-equiv="Cross-Origin-Opener-Policy"]')) {
|
|
1223
|
+
const meta = document.createElement('meta');
|
|
1224
|
+
meta.setAttribute('http-equiv', 'Cross-Origin-Opener-Policy');
|
|
1225
|
+
meta.setAttribute('content', 'same-origin-allow-popups');
|
|
1226
|
+
if (document.head) {
|
|
1227
|
+
document.head.appendChild(meta);
|
|
1228
|
+
console.log('[InAppBrowser] Added Cross-Origin-Opener-Policy meta tag');
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
console.log('[InAppBrowser] Google Pay support enhancements complete');
|
|
1233
|
+
})();
|
|
1234
|
+
""";
|
|
1235
|
+
|
|
1236
|
+
_webView.post(() -> {
|
|
1237
|
+
if (_webView != null) {
|
|
1238
|
+
try {
|
|
1239
|
+
_webView.evaluateJavascript(googlePayScript, (result) -> {
|
|
1240
|
+
Log.d("InAppBrowser", "Google Pay polyfills injected successfully");
|
|
1241
|
+
});
|
|
1242
|
+
} catch (Exception e) {
|
|
1243
|
+
Log.e("InAppBrowser", "Error injecting Google Pay polyfills: " + e.getMessage());
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
} catch (Exception e) {
|
|
1248
|
+
Log.e("InAppBrowser", "Error preparing Google Pay polyfills: " + e.getMessage());
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
private void injectPreShowScript() {
|
|
1253
|
+
// String script =
|
|
1254
|
+
// "import('https://unpkg.com/darkreader@4.9.89/darkreader.js').then(() => {DarkReader.enable({ brightness: 100, contrast: 90, sepia: 10 });window.PreLoadScriptInterface.finished()})";
|
|
1255
|
+
|
|
1256
|
+
if (preShowSemaphore != null) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
String script = String.format(
|
|
1261
|
+
"""
|
|
1262
|
+
async function preShowFunction() {
|
|
1263
|
+
%s
|
|
1264
|
+
}
|
|
1265
|
+
preShowFunction()
|
|
1266
|
+
.then(() => window.PreShowScriptInterface.success())
|
|
1267
|
+
.catch(err => {
|
|
1268
|
+
console.error('Pre show error', err);
|
|
1269
|
+
window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err)));
|
|
1270
|
+
});
|
|
1271
|
+
""",
|
|
1272
|
+
_options.getPreShowScript()
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
Log.i("InjectPreShowScript", String.format("PreShowScript script:\n%s", script));
|
|
1276
|
+
|
|
1277
|
+
preShowSemaphore = new Semaphore(0);
|
|
1278
|
+
activity.runOnUiThread(
|
|
1279
|
+
new Runnable() {
|
|
1280
|
+
@Override
|
|
1281
|
+
public void run() {
|
|
1282
|
+
if (_webView != null) {
|
|
1283
|
+
_webView.evaluateJavascript(script, null);
|
|
1284
|
+
} else {
|
|
1285
|
+
// If WebView is null, release semaphore to prevent deadlock
|
|
1286
|
+
if (preShowSemaphore != null) {
|
|
1287
|
+
preShowSemaphore.release();
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
try {
|
|
1295
|
+
if (!preShowSemaphore.tryAcquire(10, TimeUnit.SECONDS)) {
|
|
1296
|
+
Log.e("InjectPreShowScript", "PreShowScript running for over 10 seconds. The plugin will not wait any longer!");
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (preShowError != null && !preShowError.isEmpty()) {
|
|
1300
|
+
Log.e("InjectPreShowScript", "Error within the user-provided preShowFunction: " + preShowError);
|
|
1301
|
+
}
|
|
1302
|
+
} catch (InterruptedException e) {
|
|
1303
|
+
Log.e("InjectPreShowScript", "Error when calling InjectPreShowScript: " + e.getMessage());
|
|
1304
|
+
} finally {
|
|
1305
|
+
preShowSemaphore = null;
|
|
1306
|
+
preShowError = null;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
private void openFileChooser(ValueCallback<Uri[]> filePathCallback, String acceptType, boolean isMultiple) {
|
|
1311
|
+
mFilePathCallback = filePathCallback;
|
|
1312
|
+
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
|
1313
|
+
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
|
1314
|
+
|
|
1315
|
+
// Fix MIME type handling
|
|
1316
|
+
if (acceptType == null || acceptType.isEmpty() || acceptType.equals("undefined")) {
|
|
1317
|
+
acceptType = "*/*";
|
|
72
1318
|
} else {
|
|
73
|
-
|
|
1319
|
+
// Handle common web input accept types
|
|
1320
|
+
if (acceptType.equals("image/*")) {
|
|
1321
|
+
// Keep as is - image/*
|
|
1322
|
+
} else if (acceptType.contains("image/")) {
|
|
1323
|
+
// Specific image type requested but keep it general for better compatibility
|
|
1324
|
+
acceptType = "image/*";
|
|
1325
|
+
} else if (acceptType.equals("audio/*") || acceptType.contains("audio/")) {
|
|
1326
|
+
acceptType = "audio/*";
|
|
1327
|
+
} else if (acceptType.equals("video/*") || acceptType.contains("video/")) {
|
|
1328
|
+
acceptType = "video/*";
|
|
1329
|
+
} else if (acceptType.startsWith(".") || acceptType.contains(",")) {
|
|
1330
|
+
// Handle file extensions like ".pdf, .docx" by using a general mime type
|
|
1331
|
+
if (acceptType.contains(".pdf")) {
|
|
1332
|
+
acceptType = "application/pdf";
|
|
1333
|
+
} else if (acceptType.contains(".doc") || acceptType.contains(".docx")) {
|
|
1334
|
+
acceptType = "application/msword";
|
|
1335
|
+
} else if (acceptType.contains(".xls") || acceptType.contains(".xlsx")) {
|
|
1336
|
+
acceptType = "application/vnd.ms-excel";
|
|
1337
|
+
} else if (acceptType.contains(".txt") || acceptType.contains(".text")) {
|
|
1338
|
+
acceptType = "text/plain";
|
|
1339
|
+
} else {
|
|
1340
|
+
// Default for extension lists
|
|
1341
|
+
acceptType = "*/*";
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
Log.d("InAppBrowser", "File picker using MIME type: " + acceptType);
|
|
1347
|
+
intent.setType(acceptType);
|
|
1348
|
+
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultiple);
|
|
1349
|
+
|
|
1350
|
+
try {
|
|
1351
|
+
if (activity instanceof androidx.activity.ComponentActivity) {
|
|
1352
|
+
androidx.activity.ComponentActivity componentActivity = (androidx.activity.ComponentActivity) activity;
|
|
1353
|
+
componentActivity
|
|
1354
|
+
.getActivityResultRegistry()
|
|
1355
|
+
.register(
|
|
1356
|
+
"file_chooser",
|
|
1357
|
+
new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
|
|
1358
|
+
(result) -> {
|
|
1359
|
+
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
1360
|
+
Intent data = result.getData();
|
|
1361
|
+
if (data != null) {
|
|
1362
|
+
if (data.getClipData() != null) {
|
|
1363
|
+
// Handle multiple files
|
|
1364
|
+
int count = data.getClipData().getItemCount();
|
|
1365
|
+
Uri[] results = new Uri[count];
|
|
1366
|
+
for (int i = 0; i < count; i++) {
|
|
1367
|
+
results[i] = data.getClipData().getItemAt(i).getUri();
|
|
1368
|
+
}
|
|
1369
|
+
mFilePathCallback.onReceiveValue(results);
|
|
1370
|
+
} else if (data.getData() != null) {
|
|
1371
|
+
// Handle single file
|
|
1372
|
+
mFilePathCallback.onReceiveValue(new Uri[] { data.getData() });
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
} else {
|
|
1376
|
+
mFilePathCallback.onReceiveValue(null);
|
|
1377
|
+
}
|
|
1378
|
+
mFilePathCallback = null;
|
|
1379
|
+
}
|
|
1380
|
+
)
|
|
1381
|
+
.launch(Intent.createChooser(intent, "Select File"));
|
|
1382
|
+
} else {
|
|
1383
|
+
// Fallback for non-ComponentActivity
|
|
1384
|
+
activity.startActivityForResult(Intent.createChooser(intent, "Select File"), FILE_CHOOSER_REQUEST_CODE);
|
|
1385
|
+
}
|
|
1386
|
+
} catch (ActivityNotFoundException e) {
|
|
1387
|
+
// If no app can handle the specific MIME type, try with a more generic one
|
|
1388
|
+
Log.e("InAppBrowser", "No app available for type: " + acceptType + ", trying with */*");
|
|
1389
|
+
intent.setType("*/*");
|
|
1390
|
+
try {
|
|
1391
|
+
if (activity instanceof androidx.activity.ComponentActivity) {
|
|
1392
|
+
androidx.activity.ComponentActivity componentActivity = (androidx.activity.ComponentActivity) activity;
|
|
1393
|
+
componentActivity
|
|
1394
|
+
.getActivityResultRegistry()
|
|
1395
|
+
.register(
|
|
1396
|
+
"file_chooser",
|
|
1397
|
+
new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
|
|
1398
|
+
(result) -> {
|
|
1399
|
+
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
1400
|
+
Intent data = result.getData();
|
|
1401
|
+
if (data != null) {
|
|
1402
|
+
if (data.getClipData() != null) {
|
|
1403
|
+
// Handle multiple files
|
|
1404
|
+
int count = data.getClipData().getItemCount();
|
|
1405
|
+
Uri[] results = new Uri[count];
|
|
1406
|
+
for (int i = 0; i < count; i++) {
|
|
1407
|
+
results[i] = data.getClipData().getItemAt(i).getUri();
|
|
1408
|
+
}
|
|
1409
|
+
mFilePathCallback.onReceiveValue(results);
|
|
1410
|
+
} else if (data.getData() != null) {
|
|
1411
|
+
// Handle single file
|
|
1412
|
+
mFilePathCallback.onReceiveValue(new Uri[] { data.getData() });
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
} else {
|
|
1416
|
+
mFilePathCallback.onReceiveValue(null);
|
|
1417
|
+
}
|
|
1418
|
+
mFilePathCallback = null;
|
|
1419
|
+
}
|
|
1420
|
+
)
|
|
1421
|
+
.launch(Intent.createChooser(intent, "Select File"));
|
|
1422
|
+
} else {
|
|
1423
|
+
// Fallback for non-ComponentActivity
|
|
1424
|
+
activity.startActivityForResult(Intent.createChooser(intent, "Select File"), FILE_CHOOSER_REQUEST_CODE);
|
|
1425
|
+
}
|
|
1426
|
+
} catch (ActivityNotFoundException ex) {
|
|
1427
|
+
// If still failing, report error
|
|
1428
|
+
Log.e("InAppBrowser", "No app can handle file picker", ex);
|
|
1429
|
+
if (mFilePathCallback != null) {
|
|
1430
|
+
mFilePathCallback.onReceiveValue(null);
|
|
1431
|
+
mFilePathCallback = null;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
public void reload() {
|
|
1438
|
+
if (_webView == null) {
|
|
1439
|
+
Log.w("InAppBrowser", "Cannot reload - WebView is null");
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
try {
|
|
1444
|
+
// First stop any ongoing loading
|
|
1445
|
+
_webView.stopLoading();
|
|
1446
|
+
|
|
1447
|
+
// Check if there's a URL to reload
|
|
1448
|
+
String currentUrl = getUrl();
|
|
1449
|
+
if (currentUrl != null && !currentUrl.equals("about:blank")) {
|
|
1450
|
+
// Reload the current page
|
|
1451
|
+
_webView.reload();
|
|
1452
|
+
Log.d("InAppBrowser", "Reloading page: " + currentUrl);
|
|
1453
|
+
} else if (_options != null && _options.getUrl() != null) {
|
|
1454
|
+
// If webView URL is null but we have an initial URL, load that
|
|
1455
|
+
setUrl(_options.getUrl());
|
|
1456
|
+
Log.d("InAppBrowser", "Loading initial URL: " + _options.getUrl());
|
|
1457
|
+
} else {
|
|
1458
|
+
Log.w("InAppBrowser", "Cannot reload - no valid URL available");
|
|
1459
|
+
}
|
|
1460
|
+
} catch (Exception e) {
|
|
1461
|
+
Log.e("InAppBrowser", "Error during reload: " + e.getMessage());
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
public void destroy() {
|
|
1466
|
+
if (_webView != null) {
|
|
1467
|
+
_webView.destroy();
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
public String getUrl() {
|
|
1472
|
+
try {
|
|
1473
|
+
WebView webView = _webView;
|
|
1474
|
+
if (webView != null) {
|
|
1475
|
+
String url = webView.getUrl();
|
|
1476
|
+
return url != null ? url : "";
|
|
1477
|
+
}
|
|
1478
|
+
} catch (Exception e) {
|
|
1479
|
+
Log.w("InAppBrowser", "Error getting URL: " + e.getMessage());
|
|
1480
|
+
}
|
|
1481
|
+
return "";
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
public void executeScript(String script) {
|
|
1485
|
+
if (_webView == null) {
|
|
1486
|
+
Log.w("InAppBrowser", "Cannot execute script - WebView is null");
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (script == null || script.trim().isEmpty()) {
|
|
1491
|
+
Log.w("InAppBrowser", "Cannot execute empty script");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
try {
|
|
1496
|
+
_webView.evaluateJavascript(script, null);
|
|
1497
|
+
} catch (Exception e) {
|
|
1498
|
+
Log.e("InAppBrowser", "Error executing script: " + e.getMessage());
|
|
74
1499
|
}
|
|
75
|
-
}
|
|
76
1500
|
}
|
|
77
1501
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
1502
|
+
public void setUrl(String url) {
|
|
1503
|
+
if (_webView == null) {
|
|
1504
|
+
Log.w("InAppBrowser", "Cannot set URL - WebView is null");
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
81
1507
|
|
|
82
|
-
|
|
83
|
-
|
|
1508
|
+
if (url == null || url.trim().isEmpty()) {
|
|
1509
|
+
Log.w("InAppBrowser", "Cannot set empty URL");
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
84
1512
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
1513
|
+
try {
|
|
1514
|
+
Map<String, String> requestHeaders = new HashMap<>();
|
|
1515
|
+
if (_options.getHeaders() != null) {
|
|
1516
|
+
Iterator<String> keys = _options.getHeaders().keys();
|
|
1517
|
+
while (keys.hasNext()) {
|
|
1518
|
+
String key = keys.next();
|
|
1519
|
+
if (TextUtils.equals(key.toLowerCase(), "user-agent")) {
|
|
1520
|
+
_webView.getSettings().setUserAgentString(_options.getHeaders().getString(key));
|
|
1521
|
+
} else {
|
|
1522
|
+
requestHeaders.put(key, _options.getHeaders().getString(key));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
_webView.loadUrl(url, requestHeaders);
|
|
1527
|
+
} catch (Exception e) {
|
|
1528
|
+
Log.e("InAppBrowser", "Error setting URL: " + e.getMessage());
|
|
1529
|
+
}
|
|
88
1530
|
}
|
|
89
|
-
}
|
|
90
1531
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
1532
|
+
private void setTitle(String newTitleText) {
|
|
1533
|
+
TextView textView = (TextView) _toolbar.findViewById(R.id.titleText);
|
|
1534
|
+
if (_options.getVisibleTitle()) {
|
|
1535
|
+
textView.setText(newTitleText);
|
|
1536
|
+
} else {
|
|
1537
|
+
textView.setText("");
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
private void setupToolbar() {
|
|
1542
|
+
_toolbar = findViewById(R.id.tool_bar);
|
|
1543
|
+
|
|
1544
|
+
// Apply toolbar color early, for ALL toolbar types, before any view configuration
|
|
1545
|
+
if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
|
|
1546
|
+
try {
|
|
1547
|
+
int toolbarColor = Color.parseColor(_options.getToolbarColor());
|
|
1548
|
+
_toolbar.setBackgroundColor(toolbarColor);
|
|
1549
|
+
|
|
1550
|
+
// Get toolbar title and ensure it gets the right color
|
|
1551
|
+
TextView titleText = _toolbar.findViewById(R.id.titleText);
|
|
1552
|
+
|
|
1553
|
+
// Determine icon and text color
|
|
1554
|
+
int iconColor;
|
|
1555
|
+
if (_options.getToolbarTextColor() != null && !_options.getToolbarTextColor().isEmpty()) {
|
|
1556
|
+
try {
|
|
1557
|
+
iconColor = Color.parseColor(_options.getToolbarTextColor());
|
|
1558
|
+
} catch (IllegalArgumentException e) {
|
|
1559
|
+
// Fallback to automatic detection if parsing fails
|
|
1560
|
+
boolean isDarkBackground = isDarkColor(toolbarColor);
|
|
1561
|
+
iconColor = isDarkBackground ? Color.WHITE : Color.BLACK;
|
|
1562
|
+
}
|
|
1563
|
+
} else {
|
|
1564
|
+
// No explicit toolbarTextColor, use automatic detection based on background
|
|
1565
|
+
boolean isDarkBackground = isDarkColor(toolbarColor);
|
|
1566
|
+
iconColor = isDarkBackground ? Color.WHITE : Color.BLACK;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// Store for later use with navigation buttons
|
|
1570
|
+
this.iconColor = iconColor;
|
|
1571
|
+
|
|
1572
|
+
// Set title text color directly
|
|
1573
|
+
titleText.setTextColor(iconColor);
|
|
1574
|
+
|
|
1575
|
+
// Apply colors to all buttons
|
|
1576
|
+
applyColorToAllButtons(toolbarColor, iconColor);
|
|
1577
|
+
|
|
1578
|
+
// Also ensure status bar gets the color
|
|
1579
|
+
if (getWindow() != null) {
|
|
1580
|
+
// Set status bar color
|
|
1581
|
+
getWindow().setStatusBarColor(toolbarColor);
|
|
1582
|
+
|
|
1583
|
+
// Determine proper status bar text color (light or dark icons)
|
|
1584
|
+
boolean isDarkBackground = isDarkColor(toolbarColor);
|
|
1585
|
+
WindowInsetsControllerCompat insetsController = new WindowInsetsControllerCompat(
|
|
1586
|
+
getWindow(),
|
|
1587
|
+
getWindow().getDecorView()
|
|
1588
|
+
);
|
|
1589
|
+
insetsController.setAppearanceLightStatusBars(!isDarkBackground);
|
|
1590
|
+
}
|
|
1591
|
+
} catch (IllegalArgumentException e) {
|
|
1592
|
+
Log.e("InAppBrowser", "Invalid toolbar color: " + _options.getToolbarColor());
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
ImageButton closeButtonView = _toolbar.findViewById(R.id.closeButton);
|
|
1597
|
+
closeButtonView.setOnClickListener(
|
|
1598
|
+
new View.OnClickListener() {
|
|
1599
|
+
@Override
|
|
1600
|
+
public void onClick(View view) {
|
|
1601
|
+
// if closeModal true then display a native modal to check if the user is sure to close the browser
|
|
1602
|
+
if (_options.getCloseModal()) {
|
|
1603
|
+
new AlertDialog.Builder(_context)
|
|
1604
|
+
.setTitle(_options.getCloseModalTitle())
|
|
1605
|
+
.setMessage(_options.getCloseModalDescription())
|
|
1606
|
+
.setPositiveButton(
|
|
1607
|
+
_options.getCloseModalOk(),
|
|
1608
|
+
new OnClickListener() {
|
|
1609
|
+
public void onClick(DialogInterface dialog, int which) {
|
|
1610
|
+
// Close button clicked, do something
|
|
1611
|
+
String currentUrl = getUrl();
|
|
1612
|
+
dismiss();
|
|
1613
|
+
if (_options != null && _options.getCallbacks() != null) {
|
|
1614
|
+
// Notify that confirm was clicked
|
|
1615
|
+
_options.getCallbacks().confirmBtnClicked(currentUrl);
|
|
1616
|
+
_options.getCallbacks().closeEvent(currentUrl);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
)
|
|
1621
|
+
.setNegativeButton(_options.getCloseModalCancel(), null)
|
|
1622
|
+
.show();
|
|
1623
|
+
} else {
|
|
1624
|
+
String currentUrl = getUrl();
|
|
1625
|
+
dismiss();
|
|
1626
|
+
if (_options != null && _options.getCallbacks() != null) {
|
|
1627
|
+
_options.getCallbacks().closeEvent(currentUrl);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
);
|
|
1633
|
+
|
|
1634
|
+
if (_options.showArrow()) {
|
|
1635
|
+
closeButtonView.setImageResource(R.drawable.arrow_back_enabled);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// Handle reload button visibility
|
|
1639
|
+
if (_options.getShowReloadButton() && !TextUtils.equals(_options.getToolbarType(), "activity")) {
|
|
1640
|
+
View reloadButtonView = _toolbar.findViewById(R.id.reloadButton);
|
|
1641
|
+
reloadButtonView.setVisibility(View.VISIBLE);
|
|
1642
|
+
reloadButtonView.setOnClickListener(
|
|
1643
|
+
new View.OnClickListener() {
|
|
1644
|
+
@Override
|
|
1645
|
+
public void onClick(View view) {
|
|
1646
|
+
if (_webView != null) {
|
|
1647
|
+
// First stop any ongoing loading
|
|
1648
|
+
_webView.stopLoading();
|
|
1649
|
+
|
|
1650
|
+
// Check if there's a URL to reload
|
|
1651
|
+
String currentUrl = getUrl();
|
|
1652
|
+
if (currentUrl != null) {
|
|
1653
|
+
// Reload the current page
|
|
1654
|
+
_webView.reload();
|
|
1655
|
+
Log.d("InAppBrowser", "Reloading page: " + currentUrl);
|
|
1656
|
+
} else if (_options.getUrl() != null) {
|
|
1657
|
+
// If webView URL is null but we have an initial URL, load that
|
|
1658
|
+
setUrl(_options.getUrl());
|
|
1659
|
+
Log.d("InAppBrowser", "Loading initial URL: " + _options.getUrl());
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
);
|
|
101
1665
|
} else {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
1666
|
+
View reloadButtonView = _toolbar.findViewById(R.id.reloadButton);
|
|
1667
|
+
reloadButtonView.setVisibility(View.GONE);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (TextUtils.equals(_options.getToolbarType(), "activity")) {
|
|
1671
|
+
// Activity mode should ONLY have:
|
|
1672
|
+
// 1. Close button
|
|
1673
|
+
// 2. Share button (if shareSubject is provided)
|
|
1674
|
+
|
|
1675
|
+
// Hide all navigation buttons
|
|
1676
|
+
_toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
|
|
1677
|
+
_toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
|
|
1678
|
+
|
|
1679
|
+
// Hide buttonNearDone
|
|
1680
|
+
ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
|
|
1681
|
+
buttonNearDoneView.setVisibility(View.GONE);
|
|
1682
|
+
|
|
1683
|
+
// In activity mode, always make the share button visible by setting a default shareSubject if not provided
|
|
1684
|
+
if (_options.getShareSubject() == null || _options.getShareSubject().isEmpty()) {
|
|
1685
|
+
_options.setShareSubject("Share");
|
|
1686
|
+
Log.d("InAppBrowser", "Activity mode: Setting default shareSubject");
|
|
1687
|
+
}
|
|
1688
|
+
// Status bar color is already set at the top of this method, no need to set again
|
|
1689
|
+
|
|
1690
|
+
// Share button visibility is handled separately later
|
|
1691
|
+
} else if (TextUtils.equals(_options.getToolbarType(), "navigation")) {
|
|
1692
|
+
ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
|
|
1693
|
+
buttonNearDoneView.setVisibility(View.GONE);
|
|
1694
|
+
// Status bar color is already set at the top of this method, no need to set again
|
|
1695
|
+
} else if (TextUtils.equals(_options.getToolbarType(), "blank")) {
|
|
1696
|
+
_toolbar.setVisibility(View.GONE);
|
|
1697
|
+
|
|
1698
|
+
// Also set window background color to match status bar for blank toolbar
|
|
1699
|
+
View statusBarColorView = findViewById(R.id.status_bar_color_view);
|
|
1700
|
+
if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
|
|
1701
|
+
try {
|
|
1702
|
+
int toolbarColor = Color.parseColor(_options.getToolbarColor());
|
|
1703
|
+
if (getWindow() != null) {
|
|
1704
|
+
getWindow().getDecorView().setBackgroundColor(toolbarColor);
|
|
1705
|
+
}
|
|
1706
|
+
// Also set status bar color view background if available
|
|
1707
|
+
if (statusBarColorView != null) {
|
|
1708
|
+
statusBarColorView.setBackgroundColor(toolbarColor);
|
|
1709
|
+
}
|
|
1710
|
+
} catch (IllegalArgumentException e) {
|
|
1711
|
+
// Fallback to system default if color parsing fails
|
|
1712
|
+
boolean isDarkTheme = isDarkThemeEnabled();
|
|
1713
|
+
int windowBackgroundColor = isDarkTheme ? Color.BLACK : Color.WHITE;
|
|
1714
|
+
if (getWindow() != null) {
|
|
1715
|
+
getWindow().getDecorView().setBackgroundColor(windowBackgroundColor);
|
|
1716
|
+
}
|
|
1717
|
+
// Also set status bar color view background if available
|
|
1718
|
+
if (statusBarColorView != null) {
|
|
1719
|
+
statusBarColorView.setBackgroundColor(windowBackgroundColor);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
} else {
|
|
1723
|
+
// Follow system dark mode
|
|
1724
|
+
boolean isDarkTheme = isDarkThemeEnabled();
|
|
1725
|
+
int windowBackgroundColor = isDarkTheme ? Color.BLACK : Color.WHITE;
|
|
1726
|
+
if (getWindow() != null) {
|
|
1727
|
+
getWindow().getDecorView().setBackgroundColor(windowBackgroundColor);
|
|
1728
|
+
}
|
|
1729
|
+
// Also set status bar color view background if available
|
|
1730
|
+
if (statusBarColorView != null) {
|
|
1731
|
+
statusBarColorView.setBackgroundColor(windowBackgroundColor);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
} else {
|
|
1735
|
+
_toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
|
|
1736
|
+
_toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
|
|
1737
|
+
|
|
1738
|
+
// Status bar color is already set at the top of this method, no need to set again
|
|
1739
|
+
|
|
1740
|
+
Options.ButtonNearDone buttonNearDone = _options.getButtonNearDone();
|
|
1741
|
+
if (buttonNearDone != null) {
|
|
1742
|
+
ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
|
|
1743
|
+
buttonNearDoneView.setVisibility(View.VISIBLE);
|
|
1744
|
+
|
|
1745
|
+
// Handle different icon types
|
|
1746
|
+
String iconType = buttonNearDone.getIconType();
|
|
1747
|
+
if ("vector".equals(iconType)) {
|
|
1748
|
+
// Use native Android vector drawable
|
|
1749
|
+
try {
|
|
1750
|
+
String iconName = buttonNearDone.getIcon();
|
|
1751
|
+
// Convert name to Android resource ID (remove file extension if present)
|
|
1752
|
+
if (iconName.endsWith(".xml")) {
|
|
1753
|
+
iconName = iconName.substring(0, iconName.length() - 4);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Get resource ID
|
|
1757
|
+
int resourceId = _context.getResources().getIdentifier(iconName, "drawable", _context.getPackageName());
|
|
1758
|
+
|
|
1759
|
+
if (resourceId != 0) {
|
|
1760
|
+
// Set the vector drawable
|
|
1761
|
+
buttonNearDoneView.setImageResource(resourceId);
|
|
1762
|
+
// Apply color filter
|
|
1763
|
+
buttonNearDoneView.setColorFilter(iconColor);
|
|
1764
|
+
Log.d("InAppBrowser", "Successfully loaded vector drawable: " + iconName);
|
|
1765
|
+
} else {
|
|
1766
|
+
Log.e("InAppBrowser", "Vector drawable not found: " + iconName + ", using fallback");
|
|
1767
|
+
// Fallback to a common system icon
|
|
1768
|
+
buttonNearDoneView.setImageResource(android.R.drawable.ic_menu_info_details);
|
|
1769
|
+
buttonNearDoneView.setColorFilter(iconColor);
|
|
1770
|
+
}
|
|
1771
|
+
} catch (Exception e) {
|
|
1772
|
+
Log.e("InAppBrowser", "Error loading vector drawable: " + e.getMessage());
|
|
1773
|
+
// Fallback to a common system icon
|
|
1774
|
+
buttonNearDoneView.setImageResource(android.R.drawable.ic_menu_info_details);
|
|
1775
|
+
buttonNearDoneView.setColorFilter(iconColor);
|
|
1776
|
+
}
|
|
1777
|
+
} else if ("asset".equals(iconType)) {
|
|
1778
|
+
// Handle SVG from assets
|
|
1779
|
+
AssetManager assetManager = _context.getAssets();
|
|
1780
|
+
InputStream inputStream = null;
|
|
1781
|
+
try {
|
|
1782
|
+
// Try to load from public folder first
|
|
1783
|
+
String iconPath = "public/" + buttonNearDone.getIcon();
|
|
1784
|
+
try {
|
|
1785
|
+
inputStream = assetManager.open(iconPath);
|
|
1786
|
+
} catch (IOException e) {
|
|
1787
|
+
// If not found in public, try root assets
|
|
1788
|
+
try {
|
|
1789
|
+
inputStream = assetManager.open(buttonNearDone.getIcon());
|
|
1790
|
+
} catch (IOException e2) {
|
|
1791
|
+
Log.e("InAppBrowser", "SVG file not found in assets: " + buttonNearDone.getIcon());
|
|
1792
|
+
buttonNearDoneView.setVisibility(View.GONE);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// Parse and render SVG
|
|
1798
|
+
SVG svg = SVG.getFromInputStream(inputStream);
|
|
1799
|
+
if (svg == null) {
|
|
1800
|
+
Log.e("InAppBrowser", "Failed to parse SVG icon: " + buttonNearDone.getIcon());
|
|
1801
|
+
buttonNearDoneView.setVisibility(View.GONE);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// Get the dimensions from options or use SVG's size
|
|
1806
|
+
float width = buttonNearDone.getWidth() > 0 ? buttonNearDone.getWidth() : 24;
|
|
1807
|
+
float height = buttonNearDone.getHeight() > 0 ? buttonNearDone.getHeight() : 24;
|
|
1808
|
+
|
|
1809
|
+
// Get density for proper scaling
|
|
1810
|
+
float density = _context.getResources().getDisplayMetrics().density;
|
|
1811
|
+
int targetWidth = Math.round(width * density);
|
|
1812
|
+
int targetHeight = Math.round(height * density);
|
|
1813
|
+
|
|
1814
|
+
// Set document size
|
|
1815
|
+
svg.setDocumentWidth(targetWidth);
|
|
1816
|
+
svg.setDocumentHeight(targetHeight);
|
|
1817
|
+
|
|
1818
|
+
// Create a bitmap and render SVG to it for better quality
|
|
1819
|
+
Bitmap bitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
|
|
1820
|
+
Canvas canvas = new Canvas(bitmap);
|
|
1821
|
+
svg.renderToCanvas(canvas);
|
|
1822
|
+
|
|
1823
|
+
// Apply color filter to the bitmap
|
|
1824
|
+
Paint paint = new Paint();
|
|
1825
|
+
paint.setColorFilter(new PorterDuffColorFilter(iconColor, PorterDuff.Mode.SRC_IN));
|
|
1826
|
+
Canvas colorFilterCanvas = new Canvas(bitmap);
|
|
1827
|
+
colorFilterCanvas.drawBitmap(bitmap, 0, 0, paint);
|
|
1828
|
+
|
|
1829
|
+
// Set the colored bitmap as image
|
|
1830
|
+
buttonNearDoneView.setImageBitmap(bitmap);
|
|
1831
|
+
buttonNearDoneView.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
|
1832
|
+
buttonNearDoneView.setPadding(12, 12, 12, 12); // Standard button padding
|
|
1833
|
+
} catch (SVGParseException e) {
|
|
1834
|
+
Log.e("InAppBrowser", "Error loading SVG icon: " + e.getMessage(), e);
|
|
1835
|
+
buttonNearDoneView.setVisibility(View.GONE);
|
|
1836
|
+
} finally {
|
|
1837
|
+
if (inputStream != null) {
|
|
1838
|
+
try {
|
|
1839
|
+
inputStream.close();
|
|
1840
|
+
} catch (IOException e) {
|
|
1841
|
+
Log.e("InAppBrowser", "Error closing input stream: " + e.getMessage());
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
} else {
|
|
1846
|
+
// Default fallback or unsupported type
|
|
1847
|
+
Log.e("InAppBrowser", "Unsupported icon type: " + iconType);
|
|
1848
|
+
buttonNearDoneView.setVisibility(View.GONE);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// Set the click listener
|
|
1852
|
+
buttonNearDoneView.setOnClickListener((view) -> _options.getCallbacks().buttonNearDoneClicked());
|
|
1853
|
+
} else {
|
|
1854
|
+
ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
|
|
1855
|
+
buttonNearDoneView.setVisibility(View.GONE);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// Add share button functionality
|
|
1860
|
+
ImageButton shareButton = _toolbar.findViewById(R.id.shareButton);
|
|
1861
|
+
if (_options.getShareSubject() != null && !_options.getShareSubject().isEmpty()) {
|
|
1862
|
+
shareButton.setVisibility(View.VISIBLE);
|
|
1863
|
+
Log.d("InAppBrowser", "Share button should be visible, shareSubject: " + _options.getShareSubject());
|
|
1864
|
+
|
|
1865
|
+
// Apply the same color filter as other buttons to ensure visibility
|
|
1866
|
+
shareButton.setColorFilter(iconColor);
|
|
1867
|
+
|
|
1868
|
+
// The color filter is now applied in applyColorToAllButtons
|
|
1869
|
+
shareButton.setOnClickListener((view) -> {
|
|
1870
|
+
JSObject shareDisclaimer = _options.getShareDisclaimer();
|
|
1871
|
+
if (shareDisclaimer != null) {
|
|
1872
|
+
new AlertDialog.Builder(_context)
|
|
1873
|
+
.setTitle(shareDisclaimer.getString("title", "Title"))
|
|
1874
|
+
.setMessage(shareDisclaimer.getString("message", "Message"))
|
|
1875
|
+
.setPositiveButton(shareDisclaimer.getString("confirmBtn", "Confirm"), (dialog, which) -> {
|
|
1876
|
+
// Notify that confirm was clicked
|
|
1877
|
+
String currentUrl = getUrl();
|
|
1878
|
+
_options.getCallbacks().confirmBtnClicked(currentUrl);
|
|
1879
|
+
shareUrl();
|
|
1880
|
+
})
|
|
1881
|
+
.setNegativeButton(shareDisclaimer.getString("cancelBtn", "Cancel"), null)
|
|
1882
|
+
.show();
|
|
1883
|
+
} else {
|
|
1884
|
+
shareUrl();
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
} else {
|
|
1888
|
+
shareButton.setVisibility(View.GONE);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// Also color the title text
|
|
1892
|
+
TextView titleText = _toolbar.findViewById(R.id.titleText);
|
|
1893
|
+
if (titleText != null) {
|
|
1894
|
+
titleText.setTextColor(iconColor);
|
|
1895
|
+
|
|
1896
|
+
// Set the title text
|
|
1897
|
+
if (!TextUtils.isEmpty(_options.getTitle())) {
|
|
1898
|
+
this.setTitle(_options.getTitle());
|
|
1899
|
+
} else {
|
|
1900
|
+
try {
|
|
1901
|
+
URI uri = new URI(_options.getUrl());
|
|
1902
|
+
this.setTitle(uri.getHost());
|
|
1903
|
+
} catch (URISyntaxException e) {
|
|
1904
|
+
this.setTitle(_options.getTitle());
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/**
|
|
1911
|
+
* Applies background and tint colors to all buttons in the toolbar
|
|
1912
|
+
*/
|
|
1913
|
+
private void applyColorToAllButtons(int backgroundColor, int iconColor) {
|
|
1914
|
+
// Get all buttons
|
|
1915
|
+
ImageButton backButton = _toolbar.findViewById(R.id.backButton);
|
|
1916
|
+
ImageButton forwardButton = _toolbar.findViewById(R.id.forwardButton);
|
|
1917
|
+
ImageButton closeButton = _toolbar.findViewById(R.id.closeButton);
|
|
1918
|
+
ImageButton reloadButton = _toolbar.findViewById(R.id.reloadButton);
|
|
1919
|
+
ImageButton shareButton = _toolbar.findViewById(R.id.shareButton);
|
|
1920
|
+
ImageButton buttonNearDoneView = _toolbar.findViewById(R.id.buttonNearDone);
|
|
1921
|
+
|
|
1922
|
+
// Set button backgrounds
|
|
1923
|
+
backButton.setBackgroundColor(backgroundColor);
|
|
1924
|
+
forwardButton.setBackgroundColor(backgroundColor);
|
|
1925
|
+
closeButton.setBackgroundColor(backgroundColor);
|
|
1926
|
+
reloadButton.setBackgroundColor(backgroundColor);
|
|
1927
|
+
|
|
1928
|
+
// Apply tint colors to buttons
|
|
1929
|
+
backButton.setColorFilter(iconColor);
|
|
1930
|
+
forwardButton.setColorFilter(iconColor);
|
|
1931
|
+
closeButton.setColorFilter(iconColor);
|
|
1932
|
+
reloadButton.setColorFilter(iconColor);
|
|
1933
|
+
shareButton.setColorFilter(iconColor);
|
|
1934
|
+
buttonNearDoneView.setColorFilter(iconColor);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
public void handleProxyResultError(String result, String id) {
|
|
1938
|
+
Log.i("InAppBrowserProxy", String.format("handleProxyResultError: %s, ok: %s id: %s", result, false, id));
|
|
1939
|
+
ProxiedRequest proxiedRequest = proxiedRequestsHashmap.get(id);
|
|
1940
|
+
if (proxiedRequest == null) {
|
|
1941
|
+
Log.e("InAppBrowserProxy", "proxiedRequest is null");
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
proxiedRequestsHashmap.remove(id);
|
|
1945
|
+
proxiedRequest.semaphore.release();
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
public void handleProxyResultOk(JSONObject result, String id) {
|
|
1949
|
+
Log.i("InAppBrowserProxy", String.format("handleProxyResultOk: %s, ok: %s, id: %s", result, true, id));
|
|
1950
|
+
ProxiedRequest proxiedRequest = proxiedRequestsHashmap.get(id);
|
|
1951
|
+
if (proxiedRequest == null) {
|
|
1952
|
+
Log.e("InAppBrowserProxy", "proxiedRequest is null");
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
proxiedRequestsHashmap.remove(id);
|
|
1956
|
+
|
|
1957
|
+
if (result == null) {
|
|
1958
|
+
proxiedRequest.semaphore.release();
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
Map<String, String> responseHeaders = new HashMap<>();
|
|
1963
|
+
String body;
|
|
1964
|
+
int code;
|
|
1965
|
+
|
|
1966
|
+
try {
|
|
1967
|
+
body = result.getString("body");
|
|
1968
|
+
code = result.getInt("code");
|
|
1969
|
+
JSONObject headers = result.getJSONObject("headers");
|
|
1970
|
+
for (Iterator<String> it = headers.keys(); it.hasNext(); ) {
|
|
1971
|
+
String headerName = it.next();
|
|
1972
|
+
String header = headers.getString(headerName);
|
|
1973
|
+
responseHeaders.put(headerName, header);
|
|
1974
|
+
}
|
|
1975
|
+
} catch (JSONException e) {
|
|
1976
|
+
Log.e("InAppBrowserProxy", "Cannot parse OK result", e);
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
String contentType = responseHeaders.get("Content-Type");
|
|
1981
|
+
if (contentType == null) {
|
|
1982
|
+
contentType = responseHeaders.get("content-type");
|
|
1983
|
+
}
|
|
1984
|
+
if (contentType == null) {
|
|
1985
|
+
Log.e("InAppBrowserProxy", "'Content-Type' header is required");
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
if (!((100 <= code && code <= 299) || (400 <= code && code <= 599))) {
|
|
1990
|
+
Log.e("InAppBrowserProxy", String.format("Status code %s outside of the allowed range", code));
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
WebResourceResponse webResourceResponse = new WebResourceResponse(
|
|
1995
|
+
contentType,
|
|
1996
|
+
"utf-8",
|
|
1997
|
+
new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))
|
|
1998
|
+
);
|
|
1999
|
+
|
|
2000
|
+
webResourceResponse.setStatusCodeAndReasonPhrase(code, getReasonPhrase(code));
|
|
2001
|
+
proxiedRequest.response = webResourceResponse;
|
|
2002
|
+
proxiedRequest.semaphore.release();
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
private void setWebViewClient() {
|
|
2006
|
+
_webView.setWebViewClient(
|
|
2007
|
+
new WebViewClient() {
|
|
2008
|
+
/**
|
|
2009
|
+
* Checks whether the given URL is authorized based on the provided list of authorized links.
|
|
2010
|
+
* <p>
|
|
2011
|
+
* For http(s) URLs, compares only the host (ignoring "www." prefix and case).
|
|
2012
|
+
* Each entry in authorizedLinks should be a base URL (e.g., "https://example.com").
|
|
2013
|
+
* If the host of the input URL matches (case-insensitive) the host of any authorized link, returns true.
|
|
2014
|
+
* <p>
|
|
2015
|
+
* This method is intended to limit which external links can be handled as authorized app links.
|
|
2016
|
+
*
|
|
2017
|
+
* @param url The URL to check. Can be any valid absolute URL.
|
|
2018
|
+
* @param authorizedLinks List of authorized base URLs (e.g., "https://mydomain.com", "myapp://").
|
|
2019
|
+
* @return true if the URL is authorized (host matches one of the authorizedLinks); false otherwise.
|
|
2020
|
+
*/
|
|
2021
|
+
private boolean isUrlAuthorized(String url, List<String> authorizedLinks) {
|
|
2022
|
+
if (authorizedLinks == null || authorizedLinks.isEmpty() || url == null) {
|
|
2023
|
+
return false;
|
|
2024
|
+
}
|
|
2025
|
+
try {
|
|
2026
|
+
URI uri = new URI(url);
|
|
2027
|
+
String urlHost = uri.getHost();
|
|
2028
|
+
if (urlHost == null) return false;
|
|
2029
|
+
if (urlHost.startsWith("www.")) urlHost = urlHost.substring(4);
|
|
2030
|
+
for (String authorized : authorizedLinks) {
|
|
2031
|
+
URI authUri = new URI(authorized);
|
|
2032
|
+
String authHost = authUri.getHost();
|
|
2033
|
+
if (authHost == null) continue;
|
|
2034
|
+
if (authHost.startsWith("www.")) authHost = authHost.substring(4);
|
|
2035
|
+
if (urlHost.equalsIgnoreCase(authHost)) {
|
|
2036
|
+
return true;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
} catch (URISyntaxException e) {
|
|
2040
|
+
Log.e("InAppBrowser", "Invalid URI in isUrlAuthorized: " + url, e);
|
|
2041
|
+
}
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
/**
|
|
2046
|
+
* Checks if a host should be blocked based on the configured blocked hosts patterns
|
|
2047
|
+
* @param url The URL to check
|
|
2048
|
+
* @param blockedHosts The list of blocked hosts patterns
|
|
2049
|
+
* @return true if the host should be blocked, false otherwise
|
|
2050
|
+
*/
|
|
2051
|
+
private boolean shouldBlockHost(String url, List<String> blockedHosts) {
|
|
2052
|
+
Uri uri = Uri.parse(url);
|
|
2053
|
+
String host = uri.getHost();
|
|
2054
|
+
|
|
2055
|
+
if (host == null || host.isEmpty()) {
|
|
2056
|
+
return false;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
if (blockedHosts == null || blockedHosts.isEmpty()) {
|
|
2060
|
+
return false;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
String normalizedHost = host.toLowerCase();
|
|
2064
|
+
|
|
2065
|
+
for (String blockPattern : blockedHosts) {
|
|
2066
|
+
if (blockPattern != null && matchesBlockPattern(normalizedHost, blockPattern.toLowerCase())) {
|
|
2067
|
+
Log.d("InAppBrowser", "Blocked host detected: " + host);
|
|
2068
|
+
return true;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
return false;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
/**
|
|
2076
|
+
* Matches a host against a blocking pattern (supports wildcards)
|
|
2077
|
+
* @param host The normalized host to check
|
|
2078
|
+
* @param pattern The normalized blocking pattern
|
|
2079
|
+
* @return true if the host matches the pattern
|
|
2080
|
+
*/
|
|
2081
|
+
private boolean matchesBlockPattern(String host, String pattern) {
|
|
2082
|
+
if (pattern == null || pattern.isEmpty()) {
|
|
2083
|
+
return false;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// Exact match - fastest check first
|
|
2087
|
+
if (host.equals(pattern)) {
|
|
2088
|
+
return true;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// No wildcards - already checked exact match above
|
|
2092
|
+
if (!pattern.contains("*")) {
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// Handle wildcard patterns
|
|
2097
|
+
if (pattern.startsWith("*.")) {
|
|
2098
|
+
return matchesWildcardDomain(host, pattern);
|
|
2099
|
+
} else if (pattern.contains("*")) {
|
|
2100
|
+
return matchesRegexPattern(host, pattern);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
return false;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
/**
|
|
2107
|
+
* Handles simple subdomain wildcard patterns like "*.example.com"
|
|
2108
|
+
* @param host The host to check
|
|
2109
|
+
* @param pattern The wildcard pattern starting with "*."
|
|
2110
|
+
* @return true if the host matches the wildcard domain
|
|
2111
|
+
*/
|
|
2112
|
+
private boolean matchesWildcardDomain(String host, String pattern) {
|
|
2113
|
+
String domain = pattern.substring(2); // Remove "*."
|
|
2114
|
+
|
|
2115
|
+
if (domain.isEmpty()) {
|
|
2116
|
+
return false;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Match exact domain or any subdomain
|
|
2120
|
+
return host.equals(domain) || host.endsWith("." + domain);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
/**
|
|
2124
|
+
* Handles complex regex patterns with multiple wildcards
|
|
2125
|
+
* @param host The host to check
|
|
2126
|
+
* @param pattern The pattern with wildcards to convert to regex
|
|
2127
|
+
* @return true if the host matches the regex pattern
|
|
2128
|
+
*/
|
|
2129
|
+
private boolean matchesRegexPattern(String host, String pattern) {
|
|
2130
|
+
try {
|
|
2131
|
+
// Escape special regex characters except *
|
|
2132
|
+
String escapedPattern = pattern
|
|
2133
|
+
.replace("\\", "\\\\") // Must escape backslashes first
|
|
2134
|
+
.replace(".", "\\.")
|
|
2135
|
+
.replace("+", "\\+")
|
|
2136
|
+
.replace("?", "\\?")
|
|
2137
|
+
.replace("^", "\\^")
|
|
2138
|
+
.replace("$", "\\$")
|
|
2139
|
+
.replace("(", "\\(")
|
|
2140
|
+
.replace(")", "\\)")
|
|
2141
|
+
.replace("[", "\\[")
|
|
2142
|
+
.replace("]", "\\]")
|
|
2143
|
+
.replace("{", "\\{")
|
|
2144
|
+
.replace("}", "\\}")
|
|
2145
|
+
.replace("|", "\\|");
|
|
2146
|
+
|
|
2147
|
+
// Convert wildcards to regex
|
|
2148
|
+
String regexPattern = "^" + escapedPattern.replace("*", ".*") + "$";
|
|
2149
|
+
|
|
2150
|
+
return Pattern.matches(regexPattern, host);
|
|
2151
|
+
} catch (Exception e) {
|
|
2152
|
+
Log.e("InAppBrowser", "Invalid regex pattern '" + pattern + "': " + e.getMessage());
|
|
2153
|
+
return false;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
@Override
|
|
2158
|
+
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
|
2159
|
+
if (view == null || _webView == null) {
|
|
2160
|
+
return false;
|
|
2161
|
+
}
|
|
2162
|
+
Context context = view.getContext();
|
|
2163
|
+
String url = request.getUrl().toString();
|
|
2164
|
+
Log.d("InAppBrowser", "shouldOverrideUrlLoading: " + url);
|
|
2165
|
+
|
|
2166
|
+
boolean isNotHttpOrHttps = !url.startsWith("https://") && !url.startsWith("http://");
|
|
2167
|
+
|
|
2168
|
+
// If preventDeeplink is true, don't handle any non-http(s) URLs
|
|
2169
|
+
if (_options.getPreventDeeplink()) {
|
|
2170
|
+
Log.d("InAppBrowser", "preventDeeplink is true");
|
|
2171
|
+
if (isNotHttpOrHttps) {
|
|
2172
|
+
return true;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// Handle authorized app links
|
|
2177
|
+
List<String> authorizedLinks = _options.getAuthorizedAppLinks();
|
|
2178
|
+
boolean urlAuthorized = isUrlAuthorized(url, authorizedLinks);
|
|
2179
|
+
|
|
2180
|
+
Log.d("InAppBrowser", "authorizedLinks: " + authorizedLinks);
|
|
2181
|
+
Log.d("InAppBrowser", "urlAuthorized: " + urlAuthorized);
|
|
2182
|
+
|
|
2183
|
+
if (urlAuthorized) {
|
|
2184
|
+
try {
|
|
2185
|
+
Log.d("InAppBrowser", "Launching intent for authorized link: " + url);
|
|
2186
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
|
2187
|
+
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
2188
|
+
context.startActivity(intent);
|
|
2189
|
+
Log.i("InAppBrowser", "Intent started for authorized link: " + url);
|
|
2190
|
+
return true;
|
|
2191
|
+
} catch (ActivityNotFoundException e) {
|
|
2192
|
+
Log.e("InAppBrowser", "No app found to handle this authorized link", e);
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
if (isNotHttpOrHttps) {
|
|
2198
|
+
try {
|
|
2199
|
+
Intent intent;
|
|
2200
|
+
if (url.startsWith("intent://")) {
|
|
2201
|
+
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
|
|
2202
|
+
} else {
|
|
2203
|
+
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
|
2204
|
+
}
|
|
2205
|
+
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
2206
|
+
context.startActivity(intent);
|
|
2207
|
+
return true;
|
|
2208
|
+
} catch (ActivityNotFoundException | URISyntaxException e) {
|
|
2209
|
+
Log.w("InAppBrowser", "No handler for external URL: " + url, e);
|
|
2210
|
+
// Notify that a page load error occurred
|
|
2211
|
+
if (_options.getCallbacks() != null && request.isForMainFrame()) {
|
|
2212
|
+
_options.getCallbacks().pageLoadError();
|
|
2213
|
+
}
|
|
2214
|
+
return true; // prevent WebView from attempting to load the custom scheme
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// Check for blocked hosts (main-frame only) using the extracted function
|
|
2219
|
+
List<String> blockedHosts = _options.getBlockedHosts();
|
|
2220
|
+
if (blockedHosts != null && !blockedHosts.isEmpty() && request.isForMainFrame()) {
|
|
2221
|
+
Log.d("InAppBrowser", "Checking for blocked hosts (on main frame)");
|
|
2222
|
+
if (shouldBlockHost(url, blockedHosts)) {
|
|
2223
|
+
// Make sure to notify that a URL has changed even when it was blocked
|
|
2224
|
+
if (_options.getCallbacks() != null) {
|
|
2225
|
+
_options.getCallbacks().urlChangeEvent(url);
|
|
2226
|
+
}
|
|
2227
|
+
Log.d("InAppBrowser", "Navigation blocked for URL: " + url);
|
|
2228
|
+
return true; // Block the navigation
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
return false;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
@Override
|
|
2236
|
+
public void onReceivedClientCertRequest(WebView view, android.webkit.ClientCertRequest request) {
|
|
2237
|
+
Log.i("InAppBrowser", "onReceivedClientCertRequest CALLED");
|
|
2238
|
+
|
|
2239
|
+
if (request == null) {
|
|
2240
|
+
Log.e("InAppBrowser", "ClientCertRequest is null");
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (activity == null) {
|
|
2245
|
+
Log.e("InAppBrowser", "Activity is null, canceling request");
|
|
2246
|
+
try {
|
|
2247
|
+
request.cancel();
|
|
2248
|
+
} catch (Exception e) {
|
|
2249
|
+
Log.e("InAppBrowser", "Error canceling request: " + e.getMessage());
|
|
2250
|
+
}
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
try {
|
|
2255
|
+
Log.i("InAppBrowser", "Host: " + request.getHost());
|
|
2256
|
+
Log.i("InAppBrowser", "Port: " + request.getPort());
|
|
2257
|
+
Log.i("InAppBrowser", "Principals: " + java.util.Arrays.toString(request.getPrincipals()));
|
|
2258
|
+
Log.i("InAppBrowser", "KeyTypes: " + java.util.Arrays.toString(request.getKeyTypes()));
|
|
2259
|
+
|
|
2260
|
+
KeyChain.choosePrivateKeyAlias(
|
|
2261
|
+
activity,
|
|
2262
|
+
new KeyChainAliasCallback() {
|
|
2263
|
+
@Override
|
|
2264
|
+
public void alias(String alias) {
|
|
2265
|
+
if (alias != null) {
|
|
2266
|
+
try {
|
|
2267
|
+
PrivateKey privateKey = KeyChain.getPrivateKey(activity, alias);
|
|
2268
|
+
X509Certificate[] certChain = KeyChain.getCertificateChain(activity, alias);
|
|
2269
|
+
request.proceed(privateKey, certChain);
|
|
2270
|
+
Log.i("InAppBrowser", "Selected certificate: " + alias);
|
|
2271
|
+
} catch (Exception e) {
|
|
2272
|
+
try {
|
|
2273
|
+
request.cancel();
|
|
2274
|
+
} catch (Exception cancelEx) {
|
|
2275
|
+
Log.e("InAppBrowser", "Error canceling request: " + cancelEx.getMessage());
|
|
2276
|
+
}
|
|
2277
|
+
Log.e("InAppBrowser", "Error selecting certificate: " + e.getMessage());
|
|
2278
|
+
}
|
|
2279
|
+
} else {
|
|
2280
|
+
try {
|
|
2281
|
+
request.cancel();
|
|
2282
|
+
} catch (Exception e) {
|
|
2283
|
+
Log.e("InAppBrowser", "Error canceling request: " + e.getMessage());
|
|
2284
|
+
}
|
|
2285
|
+
Log.i("InAppBrowser", "No certificate found");
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
},
|
|
2289
|
+
null, // keyTypes
|
|
2290
|
+
null, // issuers
|
|
2291
|
+
request.getHost(),
|
|
2292
|
+
request.getPort(),
|
|
2293
|
+
null // alias (null = system asks user to choose)
|
|
2294
|
+
);
|
|
2295
|
+
} catch (Exception e) {
|
|
2296
|
+
Log.e("InAppBrowser", "Error in onReceivedClientCertRequest: " + e.getMessage());
|
|
2297
|
+
try {
|
|
2298
|
+
request.cancel();
|
|
2299
|
+
} catch (Exception cancelEx) {
|
|
2300
|
+
Log.e("InAppBrowser", "Error canceling request after exception: " + cancelEx.getMessage());
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
private String randomRequestId() {
|
|
2306
|
+
return UUID.randomUUID().toString();
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
private String toBase64(String raw) {
|
|
2310
|
+
String s = Base64.encodeToString(raw.getBytes(), Base64.NO_WRAP);
|
|
2311
|
+
if (s.endsWith("=")) {
|
|
2312
|
+
s = s.substring(0, s.length() - 2);
|
|
2313
|
+
}
|
|
2314
|
+
return s;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
//
|
|
2318
|
+
// void handleRedirect(String currentUrl, Response response) {
|
|
2319
|
+
// String loc = response.header("Location");
|
|
2320
|
+
// _webView.evaluateJavascript("");
|
|
2321
|
+
// }
|
|
2322
|
+
//
|
|
2323
|
+
@Override
|
|
2324
|
+
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
|
2325
|
+
if (view == null || _webView == null) {
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
Pattern pattern = _options.getProxyRequestsPattern();
|
|
2329
|
+
if (pattern == null) {
|
|
2330
|
+
return null;
|
|
2331
|
+
}
|
|
2332
|
+
Matcher matcher = pattern.matcher(request.getUrl().toString());
|
|
2333
|
+
if (!matcher.find()) {
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// Requests matches the regex
|
|
2338
|
+
if (Objects.equals(request.getMethod(), "POST")) {
|
|
2339
|
+
// Log.e("HTTP", String.format("returned null (ok) %s", request.getUrl().toString()));
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
Log.i("InAppBrowserProxy", String.format("Proxying request: %s", request.getUrl().toString()));
|
|
2344
|
+
|
|
2345
|
+
// We need to call a JS function
|
|
2346
|
+
String requestId = randomRequestId();
|
|
2347
|
+
ProxiedRequest proxiedRequest = new ProxiedRequest();
|
|
2348
|
+
addProxiedRequest(requestId, proxiedRequest);
|
|
2349
|
+
|
|
2350
|
+
// lsuakdchgbbaHandleProxiedRequest
|
|
2351
|
+
activity.runOnUiThread(
|
|
2352
|
+
new Runnable() {
|
|
2353
|
+
@Override
|
|
2354
|
+
public void run() {
|
|
2355
|
+
StringBuilder headers = new StringBuilder();
|
|
2356
|
+
Map<String, String> requestHeaders = request.getRequestHeaders();
|
|
2357
|
+
for (Map.Entry<String, String> header : requestHeaders.entrySet()) {
|
|
2358
|
+
headers.append(
|
|
2359
|
+
String.format("h[atob('%s')]=atob('%s');", toBase64(header.getKey()), toBase64(header.getValue()))
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
String jsTemplate = """
|
|
2363
|
+
try {
|
|
2364
|
+
function getHeaders() {
|
|
2365
|
+
const h = {};
|
|
2366
|
+
%s
|
|
2367
|
+
return h;
|
|
2368
|
+
}
|
|
2369
|
+
window.InAppBrowserProxyRequest(new Request(atob('%s'), {
|
|
2370
|
+
headers: getHeaders(),
|
|
2371
|
+
method: '%s'
|
|
2372
|
+
})).then(async (res) => {
|
|
2373
|
+
Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({
|
|
2374
|
+
ok: true,
|
|
2375
|
+
result: (!!res ? {
|
|
2376
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
2377
|
+
code: res.status,
|
|
2378
|
+
body: (await res.text())
|
|
2379
|
+
} : null),
|
|
2380
|
+
id: '%s'
|
|
2381
|
+
});
|
|
2382
|
+
}).catch((e) => {
|
|
2383
|
+
Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({
|
|
2384
|
+
ok: false,
|
|
2385
|
+
result: e.toString(),
|
|
2386
|
+
id: '%s'
|
|
2387
|
+
});
|
|
2388
|
+
});
|
|
2389
|
+
} catch (e) {
|
|
2390
|
+
Capacitor.Plugins.InAppBrowser.lsuakdchgbbaHandleProxiedRequest({
|
|
2391
|
+
ok: false,
|
|
2392
|
+
result: e.toString(),
|
|
2393
|
+
id: '%s'
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
""";
|
|
2397
|
+
String s = String.format(
|
|
2398
|
+
jsTemplate,
|
|
2399
|
+
headers,
|
|
2400
|
+
toBase64(request.getUrl().toString()),
|
|
2401
|
+
request.getMethod(),
|
|
2402
|
+
requestId,
|
|
2403
|
+
requestId,
|
|
2404
|
+
requestId
|
|
2405
|
+
);
|
|
2406
|
+
// Log.i("HTTP", s);
|
|
2407
|
+
capacitorWebView.evaluateJavascript(s, null);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
);
|
|
2411
|
+
|
|
2412
|
+
// 10 seconds wait max
|
|
2413
|
+
try {
|
|
2414
|
+
if (proxiedRequest.semaphore.tryAcquire(1, 10, TimeUnit.SECONDS)) {
|
|
2415
|
+
return proxiedRequest.response;
|
|
2416
|
+
} else {
|
|
2417
|
+
Log.e("InAppBrowserProxy", "Semaphore timed out");
|
|
2418
|
+
removeProxiedRequest(requestId); // prevent mem leak
|
|
2419
|
+
}
|
|
2420
|
+
} catch (InterruptedException e) {
|
|
2421
|
+
Log.e("InAppBrowserProxy", "Semaphore wait error", e);
|
|
2422
|
+
}
|
|
2423
|
+
return null;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
@Override
|
|
2427
|
+
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
|
|
2428
|
+
if (view == null || _webView == null) {
|
|
2429
|
+
if (handler != null) {
|
|
2430
|
+
handler.cancel();
|
|
2431
|
+
}
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
final String sourceUrl = _options.getUrl();
|
|
2435
|
+
final String url = view.getUrl();
|
|
2436
|
+
final JSObject credentials = _options.getCredentials();
|
|
2437
|
+
|
|
2438
|
+
if (
|
|
2439
|
+
credentials != null &&
|
|
2440
|
+
credentials.getString("username") != null &&
|
|
2441
|
+
credentials.getString("password") != null &&
|
|
2442
|
+
sourceUrl != null &&
|
|
2443
|
+
url != null
|
|
2444
|
+
) {
|
|
2445
|
+
String sourceProtocol = "";
|
|
2446
|
+
String sourceHost = "";
|
|
2447
|
+
int sourcePort = -1;
|
|
2448
|
+
try {
|
|
2449
|
+
URI uri = new URI(sourceUrl);
|
|
2450
|
+
sourceProtocol = uri.getScheme();
|
|
2451
|
+
sourceHost = uri.getHost();
|
|
2452
|
+
sourcePort = uri.getPort();
|
|
2453
|
+
if (sourcePort == -1 && Objects.equals(sourceProtocol, "https")) sourcePort = 443;
|
|
2454
|
+
else if (sourcePort == -1 && Objects.equals(sourceProtocol, "http")) sourcePort = 80;
|
|
2455
|
+
} catch (URISyntaxException e) {
|
|
2456
|
+
e.printStackTrace();
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
String protocol = "";
|
|
2460
|
+
int port = -1;
|
|
2461
|
+
try {
|
|
2462
|
+
URI uri = new URI(url);
|
|
2463
|
+
protocol = uri.getScheme();
|
|
2464
|
+
port = uri.getPort();
|
|
2465
|
+
if (port == -1 && Objects.equals(protocol, "https")) port = 443;
|
|
2466
|
+
else if (port == -1 && Objects.equals(protocol, "http")) port = 80;
|
|
2467
|
+
} catch (URISyntaxException e) {
|
|
2468
|
+
e.printStackTrace();
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
if (Objects.equals(sourceHost, host) && Objects.equals(sourceProtocol, protocol) && sourcePort == port) {
|
|
2472
|
+
final String username = Objects.requireNonNull(credentials.getString("username"));
|
|
2473
|
+
final String password = Objects.requireNonNull(credentials.getString("password"));
|
|
2474
|
+
handler.proceed(username, password);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
super.onReceivedHttpAuthRequest(view, handler, host, realm);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
@Override
|
|
2483
|
+
public void onLoadResource(WebView view, String url) {
|
|
2484
|
+
if (view == null || _webView == null) {
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
super.onLoadResource(view, url);
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
@Override
|
|
2491
|
+
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
|
2492
|
+
super.onPageStarted(view, url, favicon);
|
|
2493
|
+
if (view == null || _webView == null) {
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
try {
|
|
2497
|
+
URI uri = new URI(url);
|
|
2498
|
+
if (TextUtils.isEmpty(_options.getTitle())) {
|
|
2499
|
+
setTitle(uri.getHost());
|
|
2500
|
+
}
|
|
2501
|
+
} catch (URISyntaxException e) {
|
|
2502
|
+
// Do nothing
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
|
|
2507
|
+
if (view == null || _webView == null) {
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
if (!isReload) {
|
|
2511
|
+
_options.getCallbacks().urlChangeEvent(url);
|
|
2512
|
+
}
|
|
2513
|
+
super.doUpdateVisitedHistory(view, url, isReload);
|
|
2514
|
+
injectJavaScriptInterface();
|
|
2515
|
+
|
|
2516
|
+
// Inject Google Pay polyfills if enabled
|
|
2517
|
+
if (_options.getEnableGooglePaySupport()) {
|
|
2518
|
+
injectGooglePayPolyfills();
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
@Override
|
|
2523
|
+
public void onPageFinished(WebView view, String url) {
|
|
2524
|
+
super.onPageFinished(view, url);
|
|
2525
|
+
if (view == null || _webView == null) {
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
if (!isInitialized) {
|
|
2529
|
+
isInitialized = true;
|
|
2530
|
+
_webView.clearHistory();
|
|
2531
|
+
if (_options.isPresentAfterPageLoad()) {
|
|
2532
|
+
boolean usePreShowScript = _options.getPreShowScript() != null && !_options.getPreShowScript().isEmpty();
|
|
2533
|
+
if (!usePreShowScript) {
|
|
2534
|
+
show();
|
|
2535
|
+
_options.getPluginCall().resolve();
|
|
2536
|
+
} else {
|
|
2537
|
+
executorService.execute(
|
|
2538
|
+
new Runnable() {
|
|
2539
|
+
@Override
|
|
2540
|
+
public void run() {
|
|
2541
|
+
if (_options.getPreShowScript() != null && !_options.getPreShowScript().isEmpty()) {
|
|
2542
|
+
injectPreShowScript();
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
activity.runOnUiThread(
|
|
2546
|
+
new Runnable() {
|
|
2547
|
+
@Override
|
|
2548
|
+
public void run() {
|
|
2549
|
+
show();
|
|
2550
|
+
_options.getPluginCall().resolve();
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
} else if (_options.getPreShowScript() != null && !_options.getPreShowScript().isEmpty()) {
|
|
2560
|
+
executorService.execute(
|
|
2561
|
+
new Runnable() {
|
|
2562
|
+
@Override
|
|
2563
|
+
public void run() {
|
|
2564
|
+
injectPreShowScript();
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
ImageButton backButton = _toolbar.findViewById(R.id.backButton);
|
|
2571
|
+
if (_webView != null && _webView.canGoBack()) {
|
|
2572
|
+
backButton.setImageResource(R.drawable.arrow_back_enabled);
|
|
2573
|
+
backButton.setEnabled(true);
|
|
2574
|
+
backButton.setColorFilter(iconColor);
|
|
2575
|
+
backButton.setOnClickListener(
|
|
2576
|
+
new View.OnClickListener() {
|
|
2577
|
+
@Override
|
|
2578
|
+
public void onClick(View view) {
|
|
2579
|
+
if (_webView != null && _webView.canGoBack()) {
|
|
2580
|
+
_webView.goBack();
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
);
|
|
2585
|
+
} else {
|
|
2586
|
+
backButton.setImageResource(R.drawable.arrow_back_disabled);
|
|
2587
|
+
backButton.setEnabled(false);
|
|
2588
|
+
backButton.setColorFilter(Color.argb(128, Color.red(iconColor), Color.green(iconColor), Color.blue(iconColor)));
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
ImageButton forwardButton = _toolbar.findViewById(R.id.forwardButton);
|
|
2592
|
+
if (_webView != null && _webView.canGoForward()) {
|
|
2593
|
+
forwardButton.setImageResource(R.drawable.arrow_forward_enabled);
|
|
2594
|
+
forwardButton.setEnabled(true);
|
|
2595
|
+
forwardButton.setColorFilter(iconColor);
|
|
2596
|
+
forwardButton.setOnClickListener(
|
|
2597
|
+
new View.OnClickListener() {
|
|
2598
|
+
@Override
|
|
2599
|
+
public void onClick(View view) {
|
|
2600
|
+
if (_webView != null && _webView.canGoForward()) {
|
|
2601
|
+
_webView.goForward();
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
);
|
|
2606
|
+
} else {
|
|
2607
|
+
forwardButton.setImageResource(R.drawable.arrow_forward_disabled);
|
|
2608
|
+
forwardButton.setEnabled(false);
|
|
2609
|
+
forwardButton.setColorFilter(Color.argb(128, Color.red(iconColor), Color.green(iconColor), Color.blue(iconColor)));
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
_options.getCallbacks().pageLoaded();
|
|
2613
|
+
injectJavaScriptInterface();
|
|
2614
|
+
|
|
2615
|
+
// Inject Google Pay polyfills if enabled
|
|
2616
|
+
if (_options.getEnableGooglePaySupport()) {
|
|
2617
|
+
injectGooglePayPolyfills();
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
@Override
|
|
2622
|
+
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
|
|
2623
|
+
super.onReceivedError(view, request, error);
|
|
2624
|
+
if (view == null || _webView == null) {
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
_options.getCallbacks().pageLoadError();
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
@SuppressLint("WebViewClientOnReceivedSslError")
|
|
2631
|
+
@Override
|
|
2632
|
+
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
|
|
2633
|
+
if (view == null || _webView == null) {
|
|
2634
|
+
if (handler != null) {
|
|
2635
|
+
handler.cancel();
|
|
2636
|
+
}
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
boolean ignoreSSLUntrustedError = _options.ignoreUntrustedSSLError();
|
|
2640
|
+
if (ignoreSSLUntrustedError && error.getPrimaryError() == SslError.SSL_UNTRUSTED) handler.proceed();
|
|
2641
|
+
else {
|
|
2642
|
+
super.onReceivedSslError(view, handler, error);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
/**
|
|
2650
|
+
* Navigates back in the WebView history if possible
|
|
2651
|
+
* @return true if navigation was successful, false otherwise
|
|
2652
|
+
*/
|
|
2653
|
+
public boolean goBack() {
|
|
2654
|
+
if (_webView != null && _webView.canGoBack()) {
|
|
133
2655
|
_webView.goBack();
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
_webView.goForward();
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
View closeButton = _toolbar.findViewById(R.id.closeButton);
|
|
152
|
-
closeButton.setOnClickListener(
|
|
153
|
-
new View.OnClickListener() {
|
|
154
|
-
@Override
|
|
155
|
-
public void onClick(View view) {
|
|
156
|
-
dismiss();
|
|
157
|
-
_options.getCallbacks().closeEvent(_webView.getUrl());
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
if (TextUtils.equals(_options.getToolbarType(), "activity")) {
|
|
163
|
-
_toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
|
|
164
|
-
_toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
|
|
165
|
-
//TODO: Add share button functionality
|
|
166
|
-
} else if (TextUtils.equals(_options.getToolbarType(), "navigation")) {
|
|
167
|
-
//TODO: Remove share button when implemented
|
|
168
|
-
} else if (TextUtils.equals(_options.getToolbarType(), "blank")) {
|
|
169
|
-
_toolbar.setVisibility(View.GONE);
|
|
170
|
-
} else {
|
|
171
|
-
_toolbar.findViewById(R.id.forwardButton).setVisibility(View.GONE);
|
|
172
|
-
_toolbar.findViewById(R.id.backButton).setVisibility(View.GONE);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private void setWebViewClient() {
|
|
177
|
-
_webView.setWebViewClient(
|
|
178
|
-
new WebViewClient() {
|
|
179
|
-
@Override
|
|
180
|
-
public boolean shouldOverrideUrlLoading(
|
|
181
|
-
WebView view,
|
|
182
|
-
WebResourceRequest request
|
|
183
|
-
) {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
@Override
|
|
188
|
-
public void onLoadResource(WebView view, String url) {
|
|
189
|
-
super.onLoadResource(view, url);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
@Override
|
|
193
|
-
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
|
194
|
-
super.onPageStarted(view, url, favicon);
|
|
195
|
-
try {
|
|
196
|
-
URI uri = new URI(url);
|
|
197
|
-
setTitle(uri.getHost());
|
|
198
|
-
} catch (URISyntaxException e) {
|
|
199
|
-
// Do nothing
|
|
200
|
-
}
|
|
201
|
-
_options.getCallbacks().urlChangeEvent(url);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
@Override
|
|
205
|
-
public void onPageFinished(WebView view, String url) {
|
|
206
|
-
super.onPageFinished(view, url);
|
|
207
|
-
_options.getCallbacks().pageLoaded();
|
|
208
|
-
if (!isInitialized) {
|
|
209
|
-
isInitialized = true;
|
|
210
|
-
_webView.clearHistory();
|
|
211
|
-
if (_options.isPresentAfterPageLoad()) {
|
|
212
|
-
show();
|
|
213
|
-
_options.getPluginCall().resolve();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
ImageButton backButton = _toolbar.findViewById(R.id.backButton);
|
|
218
|
-
if (_webView.canGoBack()) {
|
|
219
|
-
backButton.setImageResource(R.drawable.arrow_back_enabled);
|
|
220
|
-
backButton.setEnabled(true);
|
|
221
|
-
} else {
|
|
222
|
-
backButton.setImageResource(R.drawable.arrow_back_disabled);
|
|
223
|
-
backButton.setEnabled(false);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
ImageButton forwardButton = _toolbar.findViewById(R.id.forwardButton);
|
|
227
|
-
if (_webView.canGoForward()) {
|
|
228
|
-
forwardButton.setImageResource(R.drawable.arrow_forward_enabled);
|
|
229
|
-
forwardButton.setEnabled(true);
|
|
230
|
-
} else {
|
|
231
|
-
forwardButton.setImageResource(R.drawable.arrow_forward_disabled);
|
|
232
|
-
forwardButton.setEnabled(false);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
_options.getCallbacks().pageLoaded();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
@Override
|
|
239
|
-
public void onReceivedError(
|
|
240
|
-
WebView view,
|
|
241
|
-
WebResourceRequest request,
|
|
242
|
-
WebResourceError error
|
|
2656
|
+
return true;
|
|
2657
|
+
}
|
|
2658
|
+
return false;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
@Override
|
|
2662
|
+
public void onBackPressed() {
|
|
2663
|
+
if (
|
|
2664
|
+
_webView != null &&
|
|
2665
|
+
_webView.canGoBack() &&
|
|
2666
|
+
(TextUtils.equals(_options.getToolbarType(), "navigation") || _options.getActiveNativeNavigationForWebview())
|
|
243
2667
|
) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
2668
|
+
_webView.goBack();
|
|
2669
|
+
} else if (!_options.getDisableGoBackOnNativeApplication()) {
|
|
2670
|
+
String currentUrl = getUrl();
|
|
2671
|
+
_options.getCallbacks().closeEvent(currentUrl);
|
|
2672
|
+
if (_webView != null) {
|
|
2673
|
+
_webView.destroy();
|
|
2674
|
+
}
|
|
2675
|
+
super.onBackPressed();
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
@Override
|
|
2680
|
+
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
2681
|
+
// Forward volume key events to the MainActivity
|
|
2682
|
+
switch (keyCode) {
|
|
2683
|
+
case KeyEvent.KEYCODE_VOLUME_UP:
|
|
2684
|
+
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
|
2685
|
+
return activity.onKeyDown(keyCode, event);
|
|
2686
|
+
}
|
|
2687
|
+
return super.onKeyDown(keyCode, event);
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
@Override
|
|
2691
|
+
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
2692
|
+
// Forward volume key events to the MainActivity
|
|
2693
|
+
switch (keyCode) {
|
|
2694
|
+
case KeyEvent.KEYCODE_VOLUME_UP:
|
|
2695
|
+
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
|
2696
|
+
return activity.onKeyUp(keyCode, event);
|
|
2697
|
+
}
|
|
2698
|
+
return super.onKeyUp(keyCode, event);
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
public static String getReasonPhrase(int statusCode) {
|
|
2702
|
+
return switch (statusCode) {
|
|
2703
|
+
case (200) -> "OK";
|
|
2704
|
+
case (201) -> "Created";
|
|
2705
|
+
case (202) -> "Accepted";
|
|
2706
|
+
case (203) -> "Non Authoritative Information";
|
|
2707
|
+
case (204) -> "No Content";
|
|
2708
|
+
case (205) -> "Reset Content";
|
|
2709
|
+
case (206) -> "Partial Content";
|
|
2710
|
+
case (207) -> "Partial Update OK";
|
|
2711
|
+
case (300) -> "Multiple Choices";
|
|
2712
|
+
case (301) -> "Moved Permanently";
|
|
2713
|
+
case (302) -> "Moved Temporarily";
|
|
2714
|
+
case (303) -> "See Other";
|
|
2715
|
+
case (304) -> "Not Modified";
|
|
2716
|
+
case (305) -> "Use Proxy";
|
|
2717
|
+
case (307) -> "Temporary Redirect";
|
|
2718
|
+
case (400) -> "Bad Request";
|
|
2719
|
+
case (401) -> "Unauthorized";
|
|
2720
|
+
case (402) -> "Payment Required";
|
|
2721
|
+
case (403) -> "Forbidden";
|
|
2722
|
+
case (404) -> "Not Found";
|
|
2723
|
+
case (405) -> "Method Not Allowed";
|
|
2724
|
+
case (406) -> "Not Acceptable";
|
|
2725
|
+
case (407) -> "Proxy Authentication Required";
|
|
2726
|
+
case (408) -> "Request Timeout";
|
|
2727
|
+
case (409) -> "Conflict";
|
|
2728
|
+
case (410) -> "Gone";
|
|
2729
|
+
case (411) -> "Length Required";
|
|
2730
|
+
case (412) -> "Precondition Failed";
|
|
2731
|
+
case (413) -> "Request Entity Too Large";
|
|
2732
|
+
case (414) -> "Request-URI Too Long";
|
|
2733
|
+
case (415) -> "Unsupported Media Type";
|
|
2734
|
+
case (416) -> "Requested Range Not Satisfiable";
|
|
2735
|
+
case (417) -> "Expectation Failed";
|
|
2736
|
+
case (418) -> "Reauthentication Required";
|
|
2737
|
+
case (419) -> "Proxy Reauthentication Required";
|
|
2738
|
+
case (422) -> "Unprocessable Entity";
|
|
2739
|
+
case (423) -> "Locked";
|
|
2740
|
+
case (424) -> "Failed Dependency";
|
|
2741
|
+
case (500) -> "Server Error";
|
|
2742
|
+
case (501) -> "Not Implemented";
|
|
2743
|
+
case (502) -> "Bad Gateway";
|
|
2744
|
+
case (503) -> "Service Unavailable";
|
|
2745
|
+
case (504) -> "Gateway Timeout";
|
|
2746
|
+
case (505) -> "HTTP Version Not Supported";
|
|
2747
|
+
case (507) -> "Insufficient Storage";
|
|
2748
|
+
default -> "";
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
@Override
|
|
2753
|
+
public void dismiss() {
|
|
2754
|
+
// First, stop any ongoing operations and disable further interactions
|
|
2755
|
+
if (_webView != null) {
|
|
2756
|
+
try {
|
|
2757
|
+
// Stop loading first to prevent any ongoing operations
|
|
2758
|
+
_webView.stopLoading();
|
|
2759
|
+
|
|
2760
|
+
// Clear any pending callbacks to prevent memory leaks
|
|
2761
|
+
if (mFilePathCallback != null) {
|
|
2762
|
+
mFilePathCallback.onReceiveValue(null);
|
|
2763
|
+
mFilePathCallback = null;
|
|
2764
|
+
}
|
|
2765
|
+
tempCameraUri = null;
|
|
2766
|
+
|
|
2767
|
+
// Clear file inputs for security/privacy before destroying WebView
|
|
2768
|
+
try {
|
|
2769
|
+
String clearInputsScript = """
|
|
2770
|
+
(function() {
|
|
2771
|
+
try {
|
|
2772
|
+
var inputs = document.querySelectorAll('input[type="file"]');
|
|
2773
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
2774
|
+
inputs[i].value = '';
|
|
2775
|
+
}
|
|
2776
|
+
return true;
|
|
2777
|
+
} catch(e) {
|
|
2778
|
+
console.log('Error clearing file inputs:', e);
|
|
2779
|
+
return false;
|
|
2780
|
+
}
|
|
2781
|
+
})();
|
|
2782
|
+
""";
|
|
2783
|
+
_webView.evaluateJavascript(clearInputsScript, null);
|
|
2784
|
+
} catch (Exception e) {
|
|
2785
|
+
Log.w("InAppBrowser", "Could not clear file inputs (WebView may be in invalid state): " + e.getMessage());
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// Remove JavaScript interfaces before destroying
|
|
2789
|
+
_webView.removeJavascriptInterface("AndroidInterface");
|
|
2790
|
+
_webView.removeJavascriptInterface("PreShowScriptInterface");
|
|
2791
|
+
_webView.removeJavascriptInterface("PrintInterface");
|
|
2792
|
+
|
|
2793
|
+
// Load blank page and cleanup
|
|
2794
|
+
_webView.loadUrl("about:blank");
|
|
2795
|
+
_webView.onPause();
|
|
2796
|
+
_webView.removeAllViews();
|
|
2797
|
+
_webView.destroy();
|
|
2798
|
+
_webView = null;
|
|
2799
|
+
} catch (Exception e) {
|
|
2800
|
+
Log.e("InAppBrowser", "Error during WebView cleanup: " + e.getMessage());
|
|
2801
|
+
// Force set to null even if cleanup failed
|
|
2802
|
+
_webView = null;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// Shutdown executor service safely
|
|
2807
|
+
if (executorService != null && !executorService.isShutdown()) {
|
|
2808
|
+
try {
|
|
2809
|
+
executorService.shutdown();
|
|
2810
|
+
if (!executorService.awaitTermination(500, TimeUnit.MILLISECONDS)) {
|
|
2811
|
+
executorService.shutdownNow();
|
|
2812
|
+
}
|
|
2813
|
+
} catch (InterruptedException e) {
|
|
2814
|
+
Thread.currentThread().interrupt();
|
|
2815
|
+
executorService.shutdownNow();
|
|
2816
|
+
} catch (Exception e) {
|
|
2817
|
+
Log.e("InAppBrowser", "Error shutting down executor: " + e.getMessage());
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// Clear any remaining proxied requests
|
|
2822
|
+
synchronized (proxiedRequestsHashmap) {
|
|
2823
|
+
proxiedRequestsHashmap.clear();
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
try {
|
|
2827
|
+
super.dismiss();
|
|
2828
|
+
} catch (Exception e) {
|
|
2829
|
+
Log.e("InAppBrowser", "Error dismissing dialog: " + e.getMessage());
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
public void addProxiedRequest(String key, ProxiedRequest request) {
|
|
2834
|
+
synchronized (proxiedRequestsHashmap) {
|
|
2835
|
+
proxiedRequestsHashmap.put(key, request);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
public ProxiedRequest getProxiedRequest(String key) {
|
|
2840
|
+
synchronized (proxiedRequestsHashmap) {
|
|
2841
|
+
ProxiedRequest request = proxiedRequestsHashmap.get(key);
|
|
2842
|
+
proxiedRequestsHashmap.remove(key);
|
|
2843
|
+
return request;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
public void removeProxiedRequest(String key) {
|
|
2848
|
+
synchronized (proxiedRequestsHashmap) {
|
|
2849
|
+
proxiedRequestsHashmap.remove(key);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
private void shareUrl() {
|
|
2854
|
+
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
|
2855
|
+
shareIntent.setType("text/plain");
|
|
2856
|
+
shareIntent.putExtra(Intent.EXTRA_SUBJECT, _options.getShareSubject());
|
|
2857
|
+
shareIntent.putExtra(Intent.EXTRA_TEXT, _options.getUrl());
|
|
2858
|
+
_context.startActivity(Intent.createChooser(shareIntent, "Share"));
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
private boolean isDarkColor(int color) {
|
|
2862
|
+
int red = Color.red(color);
|
|
2863
|
+
int green = Color.green(color);
|
|
2864
|
+
int blue = Color.blue(color);
|
|
2865
|
+
double luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255.0;
|
|
2866
|
+
return luminance < 0.5;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
private boolean isDarkThemeEnabled() {
|
|
2870
|
+
// This method checks if dark theme is currently enabled without using Configuration class
|
|
2871
|
+
try {
|
|
2872
|
+
// On Android 10+, check via resources for night mode
|
|
2873
|
+
Resources.Theme theme = _context.getTheme();
|
|
2874
|
+
TypedValue typedValue = new TypedValue();
|
|
2875
|
+
|
|
2876
|
+
if (theme.resolveAttribute(android.R.attr.isLightTheme, typedValue, true)) {
|
|
2877
|
+
// isLightTheme exists - returns true if light, false if dark
|
|
2878
|
+
return typedValue.data != 1;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// Fallback method - check background color of window
|
|
2882
|
+
if (theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true)) {
|
|
2883
|
+
int backgroundColor = typedValue.data;
|
|
2884
|
+
return isDarkColor(backgroundColor);
|
|
2885
|
+
}
|
|
2886
|
+
} catch (Exception e) {
|
|
2887
|
+
// Ignore and fallback to light theme
|
|
2888
|
+
}
|
|
2889
|
+
return false;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
private void injectDatePickerFixes() {
|
|
2893
|
+
if (_webView == null) {
|
|
2894
|
+
Log.w("InAppBrowser", "Cannot inject date picker fixes - WebView is null");
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
if (datePickerInjected) {
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
datePickerInjected = true;
|
|
2903
|
+
|
|
2904
|
+
// This script adds minimal fixes for date inputs to use Material Design
|
|
2905
|
+
String script = """
|
|
2906
|
+
(function() {
|
|
2907
|
+
try {
|
|
2908
|
+
// Find all date inputs
|
|
2909
|
+
const dateInputs = document.querySelectorAll('input[type="date"]');
|
|
2910
|
+
dateInputs.forEach(input => {
|
|
2911
|
+
// Ensure change events propagate correctly
|
|
2912
|
+
let lastValue = input.value;
|
|
2913
|
+
input.addEventListener('change', () => {
|
|
2914
|
+
try {
|
|
2915
|
+
if (input.value !== lastValue) {
|
|
2916
|
+
lastValue = input.value;
|
|
2917
|
+
// Dispatch an input event to ensure frameworks detect the change
|
|
2918
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2919
|
+
}
|
|
2920
|
+
} catch(e) {
|
|
2921
|
+
console.error('Error in date input change handler:', e);
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
});
|
|
2925
|
+
} catch(e) {
|
|
2926
|
+
console.error('Error applying date picker fixes:', e);
|
|
2927
|
+
}
|
|
2928
|
+
})();""";
|
|
2929
|
+
|
|
2930
|
+
// Execute the script in the WebView
|
|
2931
|
+
_webView.post(() -> {
|
|
2932
|
+
if (_webView != null) {
|
|
2933
|
+
try {
|
|
2934
|
+
_webView.evaluateJavascript(script, null);
|
|
2935
|
+
Log.d("InAppBrowser", "Applied minimal date picker fixes");
|
|
2936
|
+
} catch (Exception e) {
|
|
2937
|
+
Log.e("InAppBrowser", "Error injecting date picker fixes: " + e.getMessage());
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
/**
|
|
2944
|
+
* Creates a temporary URI for storing camera capture
|
|
2945
|
+
* @return URI for the temporary file or null if creation failed
|
|
2946
|
+
*/
|
|
2947
|
+
private Uri createTempImageUri() {
|
|
2948
|
+
try {
|
|
2949
|
+
String fileName = "capture_" + System.currentTimeMillis() + ".jpg";
|
|
2950
|
+
java.io.File cacheDir = _context.getCacheDir();
|
|
2951
|
+
|
|
2952
|
+
// Make sure cache directory exists
|
|
2953
|
+
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
|
|
2954
|
+
return null;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
// Create temporary file
|
|
2958
|
+
java.io.File tempFile = new java.io.File(cacheDir, fileName);
|
|
2959
|
+
if (!tempFile.createNewFile()) {
|
|
2960
|
+
return null;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// Get content URI through FileProvider
|
|
2964
|
+
try {
|
|
2965
|
+
return androidx.core.content.FileProvider.getUriForFile(_context, _context.getPackageName() + ".fileprovider", tempFile);
|
|
2966
|
+
} catch (IllegalArgumentException e) {
|
|
2967
|
+
// Try using external storage as fallback
|
|
2968
|
+
java.io.File externalCacheDir = _context.getExternalCacheDir();
|
|
2969
|
+
if (externalCacheDir != null) {
|
|
2970
|
+
tempFile = new java.io.File(externalCacheDir, fileName);
|
|
2971
|
+
final boolean newFile = tempFile.createNewFile();
|
|
2972
|
+
if (!newFile) {
|
|
2973
|
+
Log.d("InAppBrowser", "Error creating new file");
|
|
2974
|
+
}
|
|
2975
|
+
return androidx.core.content.FileProvider.getUriForFile(
|
|
2976
|
+
_context,
|
|
2977
|
+
_context.getPackageName() + ".fileprovider",
|
|
2978
|
+
tempFile
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
return null;
|
|
2983
|
+
} catch (Exception e) {
|
|
2984
|
+
return null;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
private File createImageFile() throws IOException {
|
|
2989
|
+
// Create an image file name
|
|
2990
|
+
String timeStamp = new java.text.SimpleDateFormat("yyyyMMdd_HHmmss").format(new java.util.Date());
|
|
2991
|
+
String imageFileName = "JPEG_" + timeStamp + "_";
|
|
2992
|
+
File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
|
|
2993
|
+
File image = File.createTempFile(imageFileName /* prefix */, ".jpg" /* suffix */, storageDir /* directory */);
|
|
2994
|
+
return image;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
/**
|
|
2998
|
+
* Apply dimensions to the webview window
|
|
2999
|
+
*/
|
|
3000
|
+
private void applyDimensions() {
|
|
3001
|
+
Integer width = _options.getWidth();
|
|
3002
|
+
Integer height = _options.getHeight();
|
|
3003
|
+
Integer x = _options.getX();
|
|
3004
|
+
Integer y = _options.getY();
|
|
3005
|
+
|
|
3006
|
+
WindowManager.LayoutParams params = getWindow().getAttributes();
|
|
3007
|
+
|
|
3008
|
+
// If both width and height are specified, use custom dimensions
|
|
3009
|
+
if (width != null && height != null) {
|
|
3010
|
+
params.width = (int) getPixels(width);
|
|
3011
|
+
params.height = (int) getPixels(height);
|
|
3012
|
+
params.x = (x != null) ? (int) getPixels(x) : 0;
|
|
3013
|
+
params.y = (y != null) ? (int) getPixels(y) : 0;
|
|
3014
|
+
} else if (height != null && width == null) {
|
|
3015
|
+
// If only height is specified, use custom height with fullscreen width
|
|
3016
|
+
params.width = WindowManager.LayoutParams.MATCH_PARENT;
|
|
3017
|
+
params.height = (int) getPixels(height);
|
|
3018
|
+
params.x = 0;
|
|
3019
|
+
params.y = (y != null) ? (int) getPixels(y) : 0;
|
|
3020
|
+
} else {
|
|
3021
|
+
// Default to fullscreen
|
|
3022
|
+
params.width = WindowManager.LayoutParams.MATCH_PARENT;
|
|
3023
|
+
params.height = WindowManager.LayoutParams.MATCH_PARENT;
|
|
3024
|
+
params.x = 0;
|
|
3025
|
+
params.y = 0;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
getWindow().setAttributes(params);
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
/**
|
|
3032
|
+
* Update dimensions at runtime
|
|
3033
|
+
*/
|
|
3034
|
+
public void updateDimensions(Integer width, Integer height, Integer x, Integer y) {
|
|
3035
|
+
// Update options
|
|
3036
|
+
if (width != null) {
|
|
3037
|
+
_options.setWidth(width);
|
|
3038
|
+
}
|
|
3039
|
+
if (height != null) {
|
|
3040
|
+
_options.setHeight(height);
|
|
3041
|
+
}
|
|
3042
|
+
if (x != null) {
|
|
3043
|
+
_options.setX(x);
|
|
3044
|
+
}
|
|
3045
|
+
if (y != null) {
|
|
3046
|
+
_options.setY(y);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
// Apply new dimensions
|
|
3050
|
+
applyDimensions();
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
/**
|
|
3054
|
+
* Convert density-independent pixels (dp) to actual pixels
|
|
3055
|
+
*/
|
|
3056
|
+
private float getPixels(int dp) {
|
|
3057
|
+
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, _context.getResources().getDisplayMetrics());
|
|
3058
|
+
}
|
|
262
3059
|
}
|