@livx.cc/appwrap 0.28.0 → 0.28.2
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/package.json +1 -1
- package/runtime/app/app.ts +55 -54
- package/runtime/app/shell/android-helpers.ts +6 -6
- package/runtime/app/shell/background-worker.android.ts +47 -0
- package/runtime/app/shell/background-worker.ios.ts +5 -0
- package/runtime/app/shell/background-worker.ts +7 -0
- package/runtime/app/shell/banner.ts +9 -4
- package/runtime/app/shell/bridge.ts +2 -0
- package/runtime/app/shell/custom-webview.android.ts +50 -23
- package/runtime/app/shell/custom-webview.ios.ts +90 -78
- package/runtime/app/shell/devmenu.ts +2 -2
- package/runtime/app/shell/env.ts +1 -3
- package/runtime/app/shell/events.ts +5 -4
- package/runtime/app/shell/handlers-android.ts +16 -12
- package/runtime/app/shell/handlers-background.ts +57 -78
- package/runtime/app/shell/handlers-billing.ts +90 -82
- package/runtime/app/shell/handlers-extended.ts +141 -110
- package/runtime/app/shell/handlers-fs.ts +53 -41
- package/runtime/app/shell/handlers-health.ts +27 -19
- package/runtime/app/shell/handlers-media.ts +8 -14
- package/runtime/app/shell/handlers-oauth.ts +19 -19
- package/runtime/app/shell/handlers-parity.ts +118 -89
- package/runtime/app/shell/handlers-push.ts +5 -3
- package/runtime/app/shell/handlers-reviews.ts +5 -6
- package/runtime/app/shell/handlers-scanner.ts +36 -38
- package/runtime/app/shell/handlers-speech.ts +43 -49
- package/runtime/app/shell/handlers-system.ts +1 -1
- package/runtime/app/shell/handlers.ts +4 -3
- package/runtime/app/shell/ios-frameworks.d.ts +9 -0
- package/runtime/app/shell/ios-ui-delegate.ts +112 -108
- package/runtime/app/shell/native-log.ts +1 -3
- package/runtime/app/shell/status-bar.ts +7 -4
- package/runtime/app/shell/toast.ts +1 -1
- package/runtime/references.d.ts +6 -0
- package/runtime/tsconfig.json +1 -0
- package/src/cli.ts +43 -18
- package/src/derive.ts +8 -0
- package/templates/ci/github/workflows/appwrap-pr.yml +3 -3
- package/templates/ci/github/workflows/appwrap-release-android.yml +2 -2
- package/templates/ci/github/workflows/appwrap-release-ios.yml +2 -2
package/package.json
CHANGED
package/runtime/app/app.ts
CHANGED
|
@@ -16,60 +16,61 @@ function apnsTokenToHex(deviceToken: NSData): string {
|
|
|
16
16
|
if (isIOS) {
|
|
17
17
|
// Custom delegate for URL-scheme opens. NativeScript tracks lifecycle via
|
|
18
18
|
// UIApplication notifications, so adding a delegate only extends behavior.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
72
|
-
|
|
19
|
+
@NativeClass()
|
|
20
|
+
class AppWrapAppDelegate extends UIResponder implements UIApplicationDelegate {
|
|
21
|
+
static ObjCProtocols = [UIApplicationDelegate];
|
|
22
|
+
|
|
23
|
+
applicationDidFinishLaunchingWithOptions(_app: UIApplication, launchOptions: NSDictionary<string, any>): boolean {
|
|
24
|
+
// Set the notification delegate NOW so a cold-launch tap's deep link is delivered.
|
|
25
|
+
installForegroundNotificationDelegate();
|
|
26
|
+
// BGTaskScheduler launch handlers MUST be registered here (Apple's rule) — no-op unless the
|
|
27
|
+
// backgroundTask module is active (the CLI wires the generated bootstrap to the real impl).
|
|
28
|
+
registerBackgroundLaunchHandlers();
|
|
29
|
+
const url = launchOptions?.objectForKey(UIApplicationLaunchOptionsURLKey) as NSURL | null;
|
|
30
|
+
if (url) onDeepLink(url.absoluteString ?? String(url));
|
|
31
|
+
// Cold-launch from a home-screen quick action — Apple delivers it ONLY here in launchOptions
|
|
32
|
+
// (performActionForShortcutItem is NOT called for the launch action). Buffered until handshake.
|
|
33
|
+
const shortcut = launchOptions?.objectForKey(UIApplicationLaunchOptionsShortcutItemKey) as UIApplicationShortcutItem | null;
|
|
34
|
+
if (shortcut) onShortcut(String(shortcut.type));
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
// Warm activation of a quick action (app already running/backgrounded).
|
|
38
|
+
applicationPerformActionForShortcutItemCompletionHandler(
|
|
39
|
+
_app: UIApplication,
|
|
40
|
+
shortcutItem: UIApplicationShortcutItem,
|
|
41
|
+
completionHandler: (done: boolean) => void
|
|
42
|
+
): void {
|
|
43
|
+
onShortcut(String(shortcutItem.type));
|
|
44
|
+
completionHandler(true);
|
|
45
|
+
}
|
|
46
|
+
applicationOpenURLOptions(_app: UIApplication, url: NSURL, _options: NSDictionary<string, any>): boolean {
|
|
47
|
+
console.log('AppWrap: openURL', url.absoluteString);
|
|
48
|
+
onDeepLink(url.absoluteString);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// Drives `kit.screen.orientation.lock` — UIKit reads this on every rotation.
|
|
52
|
+
applicationSupportedInterfaceOrientationsForWindow(_app: UIApplication, _window: UIWindow): UIInterfaceOrientationMask {
|
|
53
|
+
// iosOrientationMask() returns the raw bitmask as a number; the protocol types it as the enum.
|
|
54
|
+
return iosOrientationMask() as UIInterfaceOrientationMask;
|
|
55
|
+
}
|
|
56
|
+
// ── Remote push (APNs) — delivered to the AppDelegate, bridged to kit.push. ──
|
|
57
|
+
applicationDidRegisterForRemoteNotificationsWithDeviceToken(_app: UIApplication, deviceToken: NSData): void {
|
|
58
|
+
onApnsToken(apnsTokenToHex(deviceToken));
|
|
59
|
+
}
|
|
60
|
+
applicationDidFailToRegisterForRemoteNotificationsWithError(_app: UIApplication, error: NSError): void {
|
|
61
|
+
onApnsError(error?.localizedDescription ?? 'APNs registration failed');
|
|
62
|
+
}
|
|
63
|
+
// Background/silent + content-available pushes (a tapped alert routes via the UN delegate).
|
|
64
|
+
applicationDidReceiveRemoteNotificationFetchCompletionHandler(
|
|
65
|
+
_app: UIApplication,
|
|
66
|
+
userInfo: NSDictionary<any, any>,
|
|
67
|
+
completionHandler: (result: UIBackgroundFetchResult) => void
|
|
68
|
+
): void {
|
|
69
|
+
onRemoteMessage(userInfo, false);
|
|
70
|
+
completionHandler(UIBackgroundFetchResult.NewData);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
Application.ios.delegate = AppWrapAppDelegate;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
Application.run({ moduleName: 'main-page' });
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Application, Utils } from '@nativescript/core';
|
|
2
2
|
|
|
3
|
-
declare const android: any, java: any;
|
|
4
|
-
|
|
5
3
|
let nextCode = 7300;
|
|
6
4
|
|
|
7
|
-
function foregroundActivity()
|
|
5
|
+
function foregroundActivity() {
|
|
8
6
|
return Application.android.foregroundActivity ?? Application.android.startActivity;
|
|
9
7
|
}
|
|
10
8
|
|
|
@@ -20,6 +18,7 @@ export function requestPermissions(permissions: string[]): Promise<boolean> {
|
|
|
20
18
|
|
|
21
19
|
const code = nextCode++;
|
|
22
20
|
return new Promise((resolve) => {
|
|
21
|
+
// interop: NS activity-event payload is dispatched untyped via Application.android.on
|
|
23
22
|
const onResult = (args: any) => {
|
|
24
23
|
if (args.requestCode !== code) return;
|
|
25
24
|
Application.android.off(Application.android.activityRequestPermissionsEvent as any, onResult);
|
|
@@ -32,7 +31,7 @@ export function requestPermissions(permissions: string[]): Promise<boolean> {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
/** Decode a content Uri (downscaled to `maxSize` longest edge) into a JPEG data URL. */
|
|
35
|
-
export function uriToDataUrl(uri:
|
|
34
|
+
export function uriToDataUrl(uri: android.net.Uri, maxSize: number): string | null {
|
|
36
35
|
const cr = Utils.android.getApplicationContext().getContentResolver();
|
|
37
36
|
const bounds = new android.graphics.BitmapFactory.Options();
|
|
38
37
|
bounds.inJustDecodeBounds = true;
|
|
@@ -51,7 +50,7 @@ export function uriToDataUrl(uri: any, maxSize: number): string | null {
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/** Compress an Android Bitmap to a JPEG data URL. */
|
|
54
|
-
export function bitmapToDataUrl(bmp:
|
|
53
|
+
export function bitmapToDataUrl(bmp: android.graphics.Bitmap): string {
|
|
55
54
|
const baos = new java.io.ByteArrayOutputStream();
|
|
56
55
|
bmp.compress(android.graphics.Bitmap.CompressFormat.JPEG, 85, baos);
|
|
57
56
|
const b64 = android.util.Base64.encodeToString(baos.toByteArray(), android.util.Base64.NO_WRAP);
|
|
@@ -59,12 +58,13 @@ export function bitmapToDataUrl(bmp: any): string {
|
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
/** Launch an intent and resolve with its activity result. */
|
|
62
|
-
export function startActivityForResult(intent:
|
|
61
|
+
export function startActivityForResult(intent: android.content.Intent): Promise<{ resultCode: number; intent: android.content.Intent }> {
|
|
63
62
|
const activity = foregroundActivity();
|
|
64
63
|
if (!activity) return Promise.reject(Object.assign(new Error('no foreground activity'), { code: 'NOT_READY' }));
|
|
65
64
|
|
|
66
65
|
const code = nextCode++;
|
|
67
66
|
return new Promise((resolve) => {
|
|
67
|
+
// interop: NS activity-event payload is dispatched untyped via Application.android.on
|
|
68
68
|
const onResult = (args: any) => {
|
|
69
69
|
if (args.requestCode !== code) return;
|
|
70
70
|
Application.android.off(Application.android.activityResultEvent as any, onResult);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Utils } from '@nativescript/core';
|
|
2
|
+
import { runHeadless, finishRun } from './handlers-background';
|
|
3
|
+
|
|
4
|
+
// androidx (WorkManager) isn't in @nativescript/types-android — it's an AndroidX library, not the
|
|
5
|
+
// platform SDK — so it stays `any`. `java` resolves from the SDK typings; `NativeClass`/`JavaProxy`
|
|
6
|
+
// are ambient NativeScript runtime globals.
|
|
7
|
+
declare const androidx: any;
|
|
8
|
+
declare const java: any;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* WorkManager headless Worker — created by WorkManager on a background launch. `doWork()` posts to the
|
|
12
|
+
* MAIN looper (the WebView must be built + driven on the UI thread), runs the headless WebView loop for
|
|
13
|
+
* the input task id, and blocks the worker thread on a `CountDownLatch` until `backgroundTask.finish`
|
|
14
|
+
* (or a timeout). Returns success/failure; WorkManager handles periodic rescheduling.
|
|
15
|
+
*
|
|
16
|
+
* ANDROID-ONLY (`.android.ts`): a top-level `@JavaProxy` / `extends androidx.work.Worker` class
|
|
17
|
+
* dereferences Android-only globals at module load. In a SHARED file that evaluates on iOS too, that
|
|
18
|
+
* throws during ES-module instantiation → hard launch crash. Isolating it here keeps it off iOS.
|
|
19
|
+
* ⚠ DEVICE-UNVERIFIED — compiles only (see handlers-background.ts header).
|
|
20
|
+
*/
|
|
21
|
+
@NativeClass()
|
|
22
|
+
@JavaProxy('cc.livx.appwrap.AppwrapBackgroundWorker')
|
|
23
|
+
export class AppwrapBackgroundWorker extends androidx.work.Worker {
|
|
24
|
+
constructor(context: any, params: any) {
|
|
25
|
+
super(context, params);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
doWork(): any {
|
|
29
|
+
const id = this.getInputData().getString('appwrap.taskId') ?? '';
|
|
30
|
+
if (!id) return androidx.work.ListenableWorker.Result.failure();
|
|
31
|
+
|
|
32
|
+
const latch = new java.util.concurrent.CountDownLatch(1);
|
|
33
|
+
const result = { success: false };
|
|
34
|
+
Utils.dispatchToMainThread(() => {
|
|
35
|
+
runHeadless(id)
|
|
36
|
+
.then((ok: boolean) => { result.success = ok; latch.countDown(); })
|
|
37
|
+
.catch(() => { latch.countDown(); });
|
|
38
|
+
});
|
|
39
|
+
// Block the worker thread (bounded — below the WorkManager 10-min ceiling) until the JS handler
|
|
40
|
+
// finishes. A timeout returns failure so WorkManager retries on its schedule.
|
|
41
|
+
const completed = latch.await(9, java.util.concurrent.TimeUnit.MINUTES);
|
|
42
|
+
if (!completed) finishRun(id, false);
|
|
43
|
+
return completed && result.success
|
|
44
|
+
? androidx.work.ListenableWorker.Result.success()
|
|
45
|
+
: androidx.work.ListenableWorker.Result.failure();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// iOS stub. The WorkManager Worker is Android-only; handlers-background.ts STATICALLY imports
|
|
2
|
+
// `AppwrapBackgroundWorker`, so the name must resolve on iOS too — but it must carry NO `@JavaProxy` /
|
|
3
|
+
// `androidx` references (those are undefined on iOS and would crash ES-module instantiation at launch).
|
|
4
|
+
// registerAndroid() is the only consumer and never runs on iOS, so this value is never dereferenced.
|
|
5
|
+
export const AppwrapBackgroundWorker: any = undefined;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Base / iOS module for `./background-worker`. The WorkManager Worker is Android-only; the real
|
|
2
|
+
// `@JavaProxy` class lives in `background-worker.android.ts` (NS resolves the `.android.ts` override on
|
|
3
|
+
// Android). This base resolves for plain `tsc` (which doesn't know NS platform suffixes) AND serves as
|
|
4
|
+
// the iOS runtime impl — it must carry NO `@JavaProxy` / `androidx` references (undefined on iOS, they'd
|
|
5
|
+
// crash ES-module instantiation at launch). registerAndroid() — the only consumer — never runs on iOS,
|
|
6
|
+
// so this value is never dereferenced there.
|
|
7
|
+
export const AppwrapBackgroundWorker: any = undefined;
|
|
@@ -9,7 +9,7 @@ import { bridge } from './bridge';
|
|
|
9
9
|
|
|
10
10
|
let currentId: string | null = null;
|
|
11
11
|
let iosBanner: UIView | null = null;
|
|
12
|
-
let iosTapHandler: any = null;
|
|
12
|
+
let iosTapHandler: any = null; // runtime-built ObjC gesture target (see iosGestureHandlerClass)
|
|
13
13
|
let androidBanner: android.view.View | null = null;
|
|
14
14
|
|
|
15
15
|
function onTap(): void {
|
|
@@ -41,8 +41,13 @@ export function dismissBanner(): void {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// ── iOS ──────────────────────────────────────────────────────────────
|
|
44
|
-
//
|
|
45
|
-
//
|
|
44
|
+
// Target/action handler for the banner's tap + swipe-down gestures (bare selectors exposed via
|
|
45
|
+
// `exposedMethods`, no ObjC protocol). Built lazily INSIDE this iOS-only path — `interop` and
|
|
46
|
+
// `NSObject` don't exist on Android, and this file is imported on both platforms via handlers.ts, so
|
|
47
|
+
// a top-level `@NativeClass` with a `static ObjCExposedMethods = { … interop.types.void }` initializer
|
|
48
|
+
// would touch `interop` at module load and crash the Android shell (same reason handlers-scanner keeps
|
|
49
|
+
// its `exposedMethods` cancel-button target as a runtime `.extend` inside its iOS-only registrar).
|
|
50
|
+
// any — runtime-built ObjC subclass; an exposedMethods (non-protocol) target has no static type.
|
|
46
51
|
let IOSGestureHandler: any = null;
|
|
47
52
|
function iosGestureHandlerClass(): any {
|
|
48
53
|
if (!IOSGestureHandler) {
|
|
@@ -117,7 +122,7 @@ function showAndroidBanner(message: string): void {
|
|
|
117
122
|
const SWIPE = 40 * density;
|
|
118
123
|
let downX = 0, downY = 0, downT = 0;
|
|
119
124
|
tv.setOnTouchListener(new android.view.View.OnTouchListener({
|
|
120
|
-
onTouch(_v:
|
|
125
|
+
onTouch(_v: android.view.View, e: android.view.MotionEvent) {
|
|
121
126
|
switch (e.getActionMasked()) {
|
|
122
127
|
case android.view.MotionEvent.ACTION_DOWN:
|
|
123
128
|
downX = e.getRawX(); downY = e.getRawY(); downT = e.getEventTime();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isAndroid, isIOS } from '@nativescript/core';
|
|
2
2
|
import { CustomWebView } from './custom-webview';
|
|
3
3
|
|
|
4
|
+
// params: any — the bridge payload is an untyped JSON object decoded from the WebView; each handler
|
|
5
|
+
// narrows it to its own param shape at the call site.
|
|
4
6
|
export type HandlerFn = (params: any) => unknown | Promise<unknown>;
|
|
5
7
|
|
|
6
8
|
interface RequestEnvelope {
|
|
@@ -5,7 +5,8 @@ import { APPWRAP_GLOBALS_JS, NATIVE_FEEL_JS, serviceWorkerGuardJs, externalNavGu
|
|
|
5
5
|
import { envGlobalsJs } from './env';
|
|
6
6
|
import { requestPermissions } from './android-helpers';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// `android` + `java` resolve to the real types-android namespaces (no declare needed).
|
|
9
|
+
declare const androidx: any; // no NS types: androidx.webkit.* (WebViewFeature/WebViewCompat) not in the android-32 platform typings
|
|
9
10
|
|
|
10
11
|
/** Stable in-app origin — secure context, ES modules work (file:// blocks them). */
|
|
11
12
|
export const APP_ORIGIN = 'https://appwrap.local';
|
|
@@ -42,7 +43,8 @@ export class CustomWebView extends WebView {
|
|
|
42
43
|
|
|
43
44
|
initNativeView(): void {
|
|
44
45
|
super.initNativeView();
|
|
45
|
-
|
|
46
|
+
// NS core types the `.android` getter as `any`; narrow to the real WebView for this scope.
|
|
47
|
+
const wv = this.android as android.webkit.WebView;
|
|
46
48
|
if (!wv) return;
|
|
47
49
|
|
|
48
50
|
// Debug mode only: debuggable via chrome://inspect (full console + network) + keep the screen
|
|
@@ -77,7 +79,7 @@ export class CustomWebView extends WebView {
|
|
|
77
79
|
* is process-wide but harmless with our single WebView. iOS suspends rAF on its own (no-op there).
|
|
78
80
|
*/
|
|
79
81
|
setRenderingActive(active: boolean): void {
|
|
80
|
-
const wv = this.android;
|
|
82
|
+
const wv = this.android as android.webkit.WebView;
|
|
81
83
|
if (!wv) return;
|
|
82
84
|
try {
|
|
83
85
|
if (active) { wv.onResume(); wv.resumeTimers(); }
|
|
@@ -87,11 +89,11 @@ export class CustomWebView extends WebView {
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
private installDocumentStartShim(wv:
|
|
92
|
+
private installDocumentStartShim(wv: android.webkit.WebView): void {
|
|
91
93
|
try {
|
|
92
94
|
const wkt = androidx.webkit;
|
|
93
95
|
if (wkt.WebViewFeature.isFeatureSupported(wkt.WebViewFeature.DOCUMENT_START_SCRIPT)) {
|
|
94
|
-
const rules = new java.util.HashSet();
|
|
96
|
+
const rules = new java.util.HashSet<string>();
|
|
95
97
|
rules.add(APP_ORIGIN);
|
|
96
98
|
wkt.WebViewCompat.addDocumentStartJavaScript(wv, buildBootstrapJs(), rules);
|
|
97
99
|
return;
|
|
@@ -102,13 +104,20 @@ export class CustomWebView extends WebView {
|
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
private createTransportChromeClient():
|
|
107
|
+
private createTransportChromeClient(): android.webkit.WebChromeClient {
|
|
106
108
|
const owner = new WeakRef<CustomWebView>(this);
|
|
109
|
+
// NS runtime adds `.extend` on Java classes; it's not in the static typings, hence the cast.
|
|
107
110
|
const ChromeClient = (android.webkit.WebChromeClient as any).extend({
|
|
108
|
-
onJsPrompt(
|
|
111
|
+
onJsPrompt(
|
|
112
|
+
_view: android.webkit.WebView,
|
|
113
|
+
_url: string,
|
|
114
|
+
message: string,
|
|
115
|
+
_defaultValue: string,
|
|
116
|
+
result: android.webkit.JsPromptResult
|
|
117
|
+
): boolean {
|
|
109
118
|
if (typeof message === 'string' && message.startsWith(PROMPT_PREFIX)) {
|
|
110
119
|
result.confirm('');
|
|
111
|
-
const view = owner.get ? owner.get() : (owner as
|
|
120
|
+
const view = owner.get ? owner.get() : (owner as { deref(): CustomWebView }).deref();
|
|
112
121
|
view?.onAppwrapMessage?.(message.slice(PROMPT_PREFIX.length));
|
|
113
122
|
return true;
|
|
114
123
|
}
|
|
@@ -117,7 +126,7 @@ export class CustomWebView extends WebView {
|
|
|
117
126
|
|
|
118
127
|
// getUserMedia: grant the WebView's per-origin capture after ensuring the
|
|
119
128
|
// app holds the matching OS runtime permission (CAMERA / RECORD_AUDIO).
|
|
120
|
-
onPermissionRequest(request:
|
|
129
|
+
onPermissionRequest(request: android.webkit.PermissionRequest): void {
|
|
121
130
|
const PR = android.webkit.PermissionRequest;
|
|
122
131
|
const resources: string[] = Array.from(request.getResources());
|
|
123
132
|
const perms = new Set<string>();
|
|
@@ -139,14 +148,15 @@ export class CustomWebView extends WebView {
|
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
/** https://appwrap.local/<path> → file under www/; SPA fallback to the entry for extension-less misses. */
|
|
142
|
-
function createAssetServingClient():
|
|
151
|
+
function createAssetServingClient(): android.webkit.WebViewClient {
|
|
143
152
|
const wwwPath = nsPath.join(knownFolders.currentApp().path, 'www');
|
|
144
153
|
|
|
145
|
-
const respond = (filePath: string, ext: string) => {
|
|
154
|
+
const respond = (filePath: string, ext: string): android.webkit.WebResourceResponse => {
|
|
146
155
|
const stream = new java.io.FileInputStream(filePath);
|
|
147
156
|
return new android.webkit.WebResourceResponse(mimeFor(ext, filePath), 'utf-8', stream);
|
|
148
157
|
};
|
|
149
158
|
|
|
159
|
+
// (Array as any).create is the NS interop helper for native byte[]; not in lib typings.
|
|
150
160
|
const emptyStream = () => new java.io.ByteArrayInputStream((Array as any).create('byte', 0));
|
|
151
161
|
|
|
152
162
|
/**
|
|
@@ -164,10 +174,16 @@ function createAssetServingClient(): any {
|
|
|
164
174
|
* (GET/HEAD) are proxied — POST/PUT/PATCH return 501 rather than silently sending an empty body.
|
|
165
175
|
* (BodDB writes ride the WS, not HTTP POST, so this is rarely hit.)
|
|
166
176
|
*/
|
|
167
|
-
const proxyToBackend = (
|
|
177
|
+
const proxyToBackend = (
|
|
178
|
+
rel: string,
|
|
179
|
+
query: string,
|
|
180
|
+
ext: string,
|
|
181
|
+
method: string,
|
|
182
|
+
headers: java.util.Map<string, string> | null
|
|
183
|
+
): android.webkit.WebResourceResponse => {
|
|
168
184
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
169
185
|
return new android.webkit.WebResourceResponse(
|
|
170
|
-
'text/plain', 'utf-8', 501, 'Not Implemented', new java.util.HashMap(),
|
|
186
|
+
'text/plain', 'utf-8', 501, 'Not Implemented', new java.util.HashMap<string, string>(),
|
|
171
187
|
new java.io.ByteArrayInputStream(new java.lang.String(
|
|
172
188
|
'appwrap Android shell cannot proxy request bodies (shouldInterceptRequest limitation)'
|
|
173
189
|
).getBytes('UTF-8'))
|
|
@@ -178,9 +194,10 @@ function createAssetServingClient(): any {
|
|
|
178
194
|
const target = `${SHELL_CONFIG.backendOrigin.replace(/\/+$/, '')}/${rel}${query}`;
|
|
179
195
|
const pump = new java.lang.Runnable({
|
|
180
196
|
run() {
|
|
181
|
-
let is:
|
|
197
|
+
let is: java.io.InputStream | null = null;
|
|
182
198
|
try {
|
|
183
|
-
|
|
199
|
+
// openConnection() statically returns URLConnection; for http(s) it's an HttpURLConnection.
|
|
200
|
+
const conn = new java.net.URL(target).openConnection() as java.net.HttpURLConnection;
|
|
184
201
|
conn.setRequestMethod(method);
|
|
185
202
|
conn.setInstanceFollowRedirects(true);
|
|
186
203
|
conn.setConnectTimeout(15000);
|
|
@@ -209,17 +226,22 @@ function createAssetServingClient(): any {
|
|
|
209
226
|
},
|
|
210
227
|
});
|
|
211
228
|
new java.lang.Thread(pump).start();
|
|
212
|
-
const outHeaders = new java.util.HashMap();
|
|
229
|
+
const outHeaders = new java.util.HashMap<string, string>();
|
|
213
230
|
outHeaders.put('Access-Control-Allow-Origin', '*');
|
|
214
231
|
outHeaders.put('Cache-Control', 'no-cache');
|
|
215
232
|
// mime/charset best-effort from the path; status 200 (real status is unknown until the bg read).
|
|
216
233
|
return new android.webkit.WebResourceResponse(mimeFor(ext, rel), 'utf-8', 200, 'OK', outHeaders, pin);
|
|
217
234
|
};
|
|
218
235
|
|
|
236
|
+
// NS runtime adds `.extend` on Java classes; it's not in the static typings, hence the cast.
|
|
219
237
|
const Client = (android.webkit.WebViewClient as any).extend({
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
238
|
+
// Modern (API 21+) overload passes a WebResourceRequest; the legacy overload passed a string URL.
|
|
239
|
+
// We defensively handle both, so the param is the union.
|
|
240
|
+
shouldInterceptRequest(
|
|
241
|
+
_view: android.webkit.WebView,
|
|
242
|
+
request: android.webkit.WebResourceRequest | string
|
|
243
|
+
): android.webkit.WebResourceResponse | null {
|
|
244
|
+
const url: string = typeof request !== 'string' ? String(request.getUrl().toString()) : request;
|
|
223
245
|
if (!url.startsWith(APP_ORIGIN)) return null; // external — let WebView handle it
|
|
224
246
|
|
|
225
247
|
const tail = url.slice(APP_ORIGIN.length);
|
|
@@ -239,25 +261,30 @@ function createAssetServingClient(): any {
|
|
|
239
261
|
// Not a local asset → proxy to the backend (mirror of iOS), making relative
|
|
240
262
|
// backend/vendor requests same-origin under app://appwrap.local.
|
|
241
263
|
if (SHELL_CONFIG.backendOrigin) {
|
|
264
|
+
const isObj = typeof request !== 'string';
|
|
242
265
|
const method = (isObj && request.getMethod && String(request.getMethod())) || 'GET';
|
|
243
266
|
const headers = isObj && request.getRequestHeaders ? request.getRequestHeaders() : null;
|
|
244
267
|
return proxyToBackend(rel, query, ext, method.toUpperCase(), headers);
|
|
245
268
|
}
|
|
246
269
|
return new android.webkit.WebResourceResponse(
|
|
247
|
-
'text/plain', 'utf-8', 404, 'Not Found', new java.util.HashMap(), emptyStream()
|
|
270
|
+
'text/plain', 'utf-8', 404, 'Not Found', new java.util.HashMap<string, string>(), emptyStream()
|
|
248
271
|
);
|
|
249
272
|
},
|
|
250
273
|
|
|
251
|
-
onPageStarted(view:
|
|
274
|
+
onPageStarted(view: android.webkit.WebView, _url: string, _favicon: android.graphics.Bitmap): void {
|
|
252
275
|
// Fallback injection — no-op when the document-start scripts already ran
|
|
253
|
-
view.evaluateJavascript(buildBootstrapJs(), null);
|
|
276
|
+
view.evaluateJavascript(buildBootstrapJs(), null as unknown as android.webkit.ValueCallback<string>);
|
|
254
277
|
},
|
|
255
278
|
|
|
256
279
|
// `appwrap dev` points at a LAN dev server that almost always uses a self-signed / mkcert TLS
|
|
257
280
|
// cert the device's trust store doesn't know — the WebView would otherwise hard-fail with
|
|
258
281
|
// ERR_CERT_AUTHORITY_INVALID. Proceed past it ONLY in a debug build AND only when actually in
|
|
259
282
|
// server-loader (dev) mode. Production app:// builds never reach this (local assets, no TLS).
|
|
260
|
-
onReceivedSslError(
|
|
283
|
+
onReceivedSslError(
|
|
284
|
+
_view: android.webkit.WebView,
|
|
285
|
+
handler: android.webkit.SslErrorHandler,
|
|
286
|
+
error: android.net.http.SslError
|
|
287
|
+
): void {
|
|
261
288
|
if (SHELL_CONFIG.debug && SHELL_CONFIG.loader === 'server') {
|
|
262
289
|
console.warn('AppWrap: proceeding past dev-server SSL error (debug+server only):', String(error));
|
|
263
290
|
handler.proceed();
|