frida-ui-runtime 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +181 -0
  2. package/lib/android-listener.d.ts +20 -0
  3. package/lib/android-listener.js +92 -0
  4. package/lib/android-runtime.d.ts +25 -0
  5. package/lib/android-runtime.js +310 -0
  6. package/lib/api.d.ts +1 -0
  7. package/lib/api.js +156 -0
  8. package/lib/component/button.d.ts +11 -0
  9. package/lib/component/button.js +61 -0
  10. package/lib/component/category.d.ts +9 -0
  11. package/lib/component/category.js +57 -0
  12. package/lib/component/checkBox.d.ts +33 -0
  13. package/lib/component/checkBox.js +276 -0
  14. package/lib/component/collapsible.d.ts +19 -0
  15. package/lib/component/collapsible.js +197 -0
  16. package/lib/component/image.d.ts +18 -0
  17. package/lib/component/image.js +85 -0
  18. package/lib/component/infoCardBox.d.ts +13 -0
  19. package/lib/component/infoCardBox.js +68 -0
  20. package/lib/component/input.d.ts +41 -0
  21. package/lib/component/input.js +342 -0
  22. package/lib/component/selector.d.ts +31 -0
  23. package/lib/component/selector.js +246 -0
  24. package/lib/component/slider.d.ts +18 -0
  25. package/lib/component/slider.js +156 -0
  26. package/lib/component/style/style.d.ts +4 -0
  27. package/lib/component/style/style.js +325 -0
  28. package/lib/component/style/theme.d.ts +33 -0
  29. package/lib/component/style/theme.js +24 -0
  30. package/lib/component/switch.d.ts +12 -0
  31. package/lib/component/switch.js +117 -0
  32. package/lib/component/text.d.ts +9 -0
  33. package/lib/component/text.js +36 -0
  34. package/lib/component/ui-components.d.ts +34 -0
  35. package/lib/component/ui-components.js +108 -0
  36. package/lib/component/views/header-view.d.ts +20 -0
  37. package/lib/component/views/header-view.js +198 -0
  38. package/lib/component/views/log-view.d.ts +41 -0
  39. package/lib/component/views/log-view.js +511 -0
  40. package/lib/component/views/tabs-view.d.ts +41 -0
  41. package/lib/component/views/tabs-view.js +334 -0
  42. package/lib/constant-config.d.ts +5 -0
  43. package/lib/constant-config.js +3 -0
  44. package/lib/event-emitter.d.ts +10 -0
  45. package/lib/event-emitter.js +52 -0
  46. package/lib/float-menu.d.ts +103 -0
  47. package/lib/float-menu.js +648 -0
  48. package/lib/index.d.ts +17 -0
  49. package/lib/index.js +16 -0
  50. package/lib/java-runtime.d.ts +2 -0
  51. package/lib/java-runtime.js +2 -0
  52. package/lib/logger.d.ts +43 -0
  53. package/lib/logger.js +200 -0
  54. package/lib/runtime/component-registry.d.ts +24 -0
  55. package/lib/runtime/component-registry.js +48 -0
  56. package/lib/runtime/launch-sequence.d.ts +5 -0
  57. package/lib/runtime/launch-sequence.js +20 -0
  58. package/lib/runtime/lifecycle-controller.d.ts +9 -0
  59. package/lib/runtime/lifecycle-controller.js +31 -0
  60. package/lib/runtime/listener-registry.d.ts +7 -0
  61. package/lib/runtime/listener-registry.js +40 -0
  62. package/lib/runtime/overlay-options.d.ts +9 -0
  63. package/lib/runtime/overlay-options.js +42 -0
  64. package/lib/runtime/safe-runtime.d.ts +3 -0
  65. package/lib/runtime/safe-runtime.js +20 -0
  66. package/lib/utils.d.ts +8 -0
  67. package/lib/utils.js +13 -0
  68. package/package.json +43 -0
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # frida-ui-runtime
2
+
3
+ Lifecycle-safe Android overlay UI runtime for Frida scripts.
4
+
5
+ The runtime creates native Android views through the consumer-installed
6
+ `frida-java-bridge` peer dependency. The consumer selects the bridge version;
7
+ the runtime validates the capabilities it needs and never depends on a
8
+ REPL-provided global `Java` object.
9
+
10
+ ## Requirements
11
+
12
+ - Frida with Java support in the target Android process
13
+ - Overlay permission for the target application
14
+ - Android `WindowManager` access
15
+ - `frida-compile` for bundling TypeScript entrypoints
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install frida-ui-runtime frida-java-bridge
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ import {
27
+ Button,
28
+ FloatMenu,
29
+ Slider,
30
+ Switch,
31
+ TextView,
32
+ } from "frida-ui-runtime";
33
+
34
+ async function main(): Promise<void> {
35
+ const menu = await FloatMenu.launch({
36
+ title: "Runtime Tools",
37
+ width: 820,
38
+ height: 1100,
39
+ icon: {
40
+ base64: iconBase64,
41
+ width: 160,
42
+ height: 160,
43
+ },
44
+ tabs: [
45
+ { id: "controls", label: "Controls" },
46
+ { id: "status", label: "Status" },
47
+ ],
48
+ activeTab: "controls",
49
+ }, async menu => {
50
+ await menu.addComponent(
51
+ new Switch("enabled", "Enabled", false),
52
+ "controls",
53
+ );
54
+ await menu.addComponent(
55
+ new Slider("level", "Level", 0, 10, 3),
56
+ "controls",
57
+ );
58
+ await menu.addComponent(
59
+ new Button("run", "Run", "primary", () => console.log("run")),
60
+ "controls",
61
+ );
62
+ await menu.addComponent(
63
+ new TextView("status", "Ready"),
64
+ "status",
65
+ );
66
+ });
67
+
68
+ // Components remain dynamic after the floating icon is visible.
69
+ await menu.addComponent(new TextView("late", "Added later"), "status");
70
+ }
71
+
72
+ void main().catch(error => console.error(error));
73
+ ```
74
+
75
+ The entrypoint runs directly in a Gadget local script and does not require a
76
+ REPL. `FloatMenu.launch()` waits for the consumer-provided Java Bridge, Android
77
+ application context, and main thread. It shows the floating icon only after the
78
+ setup callback completes.
79
+
80
+ `frida-java-bridge` is an unrestricted peer dependency. Install one bridge
81
+ version in the consuming project and let the package manager resolve both the
82
+ application and this runtime to that instance. Version `7.0.4` is only the
83
+ library's development and regression-test baseline.
84
+
85
+ At startup the runtime checks for `use`, `scheduleOnMainThread`, and either
86
+ `performNow` or `perform`. An incompatible bridge fails early with
87
+ `JavaBridgeCompatibilityError` and a list of missing capabilities.
88
+
89
+ ## Lifecycle
90
+
91
+ `FloatMenu` serializes lifecycle operations so concurrent calls cannot attach or detach the same window twice.
92
+
93
+ ```typescript
94
+ await menu.mount(); // Create and attach both windows, initially hidden
95
+ await menu.present("icon"); // Show only the floating icon
96
+ await menu.present("menu"); // Show only the menu
97
+ await menu.conceal(); // Hide visually; keep the icon hotspot touchable
98
+ await menu.toggle(); // Toggle icon/menu presentation
99
+ await menu.dispose(); // Remove windows and release listeners/components
100
+ ```
101
+
102
+ `FloatMenu.create()` remains available as a low-level API that returns a hidden
103
+ menu. Prefer `FloatMenu.launch()` for Gadget entrypoints. Calling `present()`
104
+ without a mode defaults to the floating icon.
105
+
106
+ The minimize button returns to the visible icon. The close button calls
107
+ `conceal()`: both surfaces disappear visually, but the icon window stays
108
+ transparent and touchable at its previous position. Tapping that location opens
109
+ the menu directly. The close interaction also shows a Toast explaining where to
110
+ tap. The views and component state remain mounted throughout.
111
+
112
+ Use `dispose()` only when the overlay is no longer needed and its state may be
113
+ released.
114
+
115
+ ## State
116
+
117
+ ```typescript
118
+ const snapshot = menu.captureState();
119
+ await menu.conceal();
120
+
121
+ await menu.present("menu");
122
+ menu.restoreState(snapshot);
123
+ ```
124
+
125
+ Component IDs must be unique. Duplicate IDs are rejected instead of silently replacing an existing component.
126
+
127
+ Components can be added or removed after launch, including while the overlay is
128
+ concealed:
129
+
130
+ ```typescript
131
+ await menu.conceal();
132
+ await menu.addComponent(new TextView("dynamic", "Added while hidden"));
133
+ await menu.removeComponent("dynamic");
134
+ await menu.present();
135
+ ```
136
+
137
+ ## Overlay Permission
138
+
139
+ Permission is checked before any window is attached. If permission is missing,
140
+ the runtime shows a Toast and throws `OverlayPermissionError`. Override the
141
+ Toast text with `permissionDeniedMessage` when needed.
142
+
143
+ ## Custom Icon
144
+
145
+ `icon.base64` accepts raw Base64 or a `data:image/...;base64,` value. File paths
146
+ and network URLs are intentionally unsupported. Invalid image data fails launch
147
+ with `InvalidOverlayIconError` instead of creating a blank icon window.
148
+
149
+ ## Components
150
+
151
+ The root package exports:
152
+
153
+ - `Button`
154
+ - `Category`
155
+ - `CheckBoxGroup`
156
+ - `Collapsible`
157
+ - `NumberInput`
158
+ - `Selector`
159
+ - `Slider`
160
+ - `Switch`
161
+ - `TextInput`
162
+ - `TextView` (`Text` alias)
163
+ - `UIComponent` for custom controls
164
+
165
+ All component callbacks are isolated from the Java listener boundary. Callback failures are logged instead of escaping into Android's UI thread.
166
+
167
+ ## Build And Test
168
+
169
+ ```bash
170
+ npm test
171
+ npx frida-compile tests/device/overlay-smoke.ts -o tests/device/overlay-smoke.js
172
+ ```
173
+
174
+ The repository includes an independent Android host app in `android-test-app`. Device testing does not load an application's production Frida script.
175
+
176
+ ## Compatibility Notes
177
+
178
+ - Window type selection checks available framework fields instead of trusting spoofable `SDK_INT` values.
179
+ - Application context lookup falls back across `ActivityThread` and `AppGlobals`.
180
+ - `WindowManager` wrappers are validated for `addView`, `updateViewLayout`, and `removeView`. This handles environments where a module such as Guise allows an interface cast but hides the inherited `ViewManager` methods.
181
+ - Display metrics are validated before use and fall back to real display metrics when spoofed values are invalid.
@@ -0,0 +1,20 @@
1
+ type JavaCallback = (...args: any[]) => any;
2
+ export interface JavaListenerHandle {
3
+ readonly instance: any;
4
+ $new(): any;
5
+ dispose(): void;
6
+ }
7
+ export interface JavaListenerOptions {
8
+ key: string;
9
+ owner?: string;
10
+ interfaceClass: any;
11
+ callbacks: Record<string, JavaCallback>;
12
+ fallback?: Record<string, any>;
13
+ }
14
+ export declare function createJavaListener(options: JavaListenerOptions): JavaListenerHandle;
15
+ export declare function disposeJavaListenerOwner(owner: string): void;
16
+ export declare function getJavaListenerDiagnostics(): {
17
+ pools: number;
18
+ callbacks: number;
19
+ };
20
+ export {};
@@ -0,0 +1,92 @@
1
+ import Java from "./java-runtime.js";
2
+ import { Logger } from "./logger.js";
3
+ const pools = new Map();
4
+ const owners = new Map();
5
+ const sessionSuffix = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
6
+ let nextListenerId = 1;
7
+ function sanitizeClassSegment(value) {
8
+ const sanitized = value.replace(/[^A-Za-z0-9_]/g, "_");
9
+ return /^[A-Za-z_]/.test(sanitized) ? sanitized : `L_${sanitized}`;
10
+ }
11
+ function createPool(options) {
12
+ const callbacks = new Map();
13
+ const fallback = options.fallback ?? {};
14
+ const methods = {};
15
+ for (const methodName of Object.keys(options.callbacks)) {
16
+ methods[methodName] = function (...args) {
17
+ const callbackId = String(this.callbackId.value);
18
+ const callback = callbacks.get(callbackId)?.[methodName];
19
+ if (!callback)
20
+ return fallback[methodName];
21
+ try {
22
+ return callback(...args);
23
+ }
24
+ catch (error) {
25
+ Logger.instance.error(`[JavaListener:${options.key}.${methodName}] callback failed:`, error);
26
+ return fallback[methodName];
27
+ }
28
+ };
29
+ }
30
+ const listenerClass = Java.registerClass({
31
+ name: `com.fridauiruntime.listener.${sanitizeClassSegment(options.key)}_${sessionSuffix}`,
32
+ implements: [options.interfaceClass],
33
+ fields: { callbackId: "java.lang.String" },
34
+ methods,
35
+ });
36
+ return { listenerClass, callbacks, fallback };
37
+ }
38
+ export function createJavaListener(options) {
39
+ const methodNames = Object.keys(options.callbacks).sort().join(",");
40
+ const poolKey = `${options.key}:${methodNames}`;
41
+ let pool = pools.get(poolKey);
42
+ if (!pool) {
43
+ pool = createPool(options);
44
+ pools.set(poolKey, pool);
45
+ }
46
+ const callbackId = String(nextListenerId++);
47
+ pool.callbacks.set(callbackId, options.callbacks);
48
+ const instance = pool.listenerClass.$new();
49
+ instance.callbackId.value = callbackId;
50
+ let disposed = false;
51
+ const handle = {
52
+ instance,
53
+ $new: () => instance,
54
+ dispose() {
55
+ if (disposed)
56
+ return;
57
+ disposed = true;
58
+ pool.callbacks.delete(callbackId);
59
+ const owned = options.owner ? owners.get(options.owner) : undefined;
60
+ owned?.delete(handle.dispose);
61
+ if (options.owner && owned?.size === 0)
62
+ owners.delete(options.owner);
63
+ try {
64
+ instance.callbackId.value = "";
65
+ }
66
+ catch { }
67
+ },
68
+ };
69
+ if (options.owner) {
70
+ let owned = owners.get(options.owner);
71
+ if (!owned) {
72
+ owned = new Set();
73
+ owners.set(options.owner, owned);
74
+ }
75
+ owned.add(handle.dispose);
76
+ }
77
+ return handle;
78
+ }
79
+ export function disposeJavaListenerOwner(owner) {
80
+ const owned = owners.get(owner);
81
+ if (!owned)
82
+ return;
83
+ owners.delete(owner);
84
+ for (const dispose of Array.from(owned))
85
+ dispose();
86
+ }
87
+ export function getJavaListenerDiagnostics() {
88
+ let callbacks = 0;
89
+ for (const pool of pools.values())
90
+ callbacks += pool.callbacks.size;
91
+ return { pools: pools.size, callbacks };
92
+ }
@@ -0,0 +1,25 @@
1
+ export interface JavaRuntime {
2
+ available?: boolean;
3
+ use(name: string): any;
4
+ cast?(value: any, klass: any): any;
5
+ perform?(callback: () => void): void;
6
+ performNow?(callback: () => void): void;
7
+ scheduleOnMainThread?(callback: () => void): void;
8
+ }
9
+ export declare class JavaBridgeCompatibilityError extends Error {
10
+ constructor(missingCapabilities: string[]);
11
+ }
12
+ export declare function assertJavaBridgeCompatible(java: Partial<JavaRuntime> | null | undefined): asserts java is JavaRuntime;
13
+ export declare function waitForJavaAvailable(java: JavaRuntime, timeoutMs?: number, intervalMs?: number): Promise<void>;
14
+ export declare function runWhenJavaReady<T>(java: JavaRuntime, operation: () => T, timeoutMs?: number, label?: string): Promise<T>;
15
+ export declare function scheduleOnMainThread<T>(java: JavaRuntime, operation: () => T, timeoutMs?: number, label?: string): Promise<T>;
16
+ export declare function getApplicationContext(java: JavaRuntime): any;
17
+ export declare function waitForApplicationContext(java: JavaRuntime, timeoutMs?: number, intervalMs?: number): Promise<any>;
18
+ export declare function getWindowManager(java: JavaRuntime, context: any): any;
19
+ export declare function readDisplaySize(context: any, windowManager: any): {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ export declare function readDisplayDensity(context: any): number;
24
+ export declare function getOverlayWindowType(layoutParams: any): number;
25
+ export declare function ensureWindowNotFocusable(params: any, layoutParams: any): boolean;
@@ -0,0 +1,310 @@
1
+ export class JavaBridgeCompatibilityError extends Error {
2
+ constructor(missingCapabilities) {
3
+ super(`The selected frida-java-bridge is missing required capabilities: ${missingCapabilities.join(", ")}`);
4
+ this.name = "JavaBridgeCompatibilityError";
5
+ }
6
+ }
7
+ export function assertJavaBridgeCompatible(java) {
8
+ const missing = [];
9
+ if (!java || typeof java.use !== "function")
10
+ missing.push("use");
11
+ if (!java
12
+ || (typeof java.performNow !== "function" && typeof java.perform !== "function")) {
13
+ missing.push("performNow or perform");
14
+ }
15
+ if (!java || typeof java.scheduleOnMainThread !== "function") {
16
+ missing.push("scheduleOnMainThread");
17
+ }
18
+ if (missing.length > 0)
19
+ throw new JavaBridgeCompatibilityError(missing);
20
+ }
21
+ function performImmediately(java, callback) {
22
+ const performNow = java.performNow;
23
+ if (performNow) {
24
+ performNow.call(java, callback);
25
+ return;
26
+ }
27
+ const perform = java.perform;
28
+ if (perform)
29
+ perform.call(java, callback);
30
+ else
31
+ callback();
32
+ }
33
+ export function waitForJavaAvailable(java, timeoutMs = 30000, intervalMs = 50) {
34
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
35
+ return Promise.reject(new RangeError("timeoutMs must be greater than zero"));
36
+ }
37
+ const deadline = Date.now() + timeoutMs;
38
+ return new Promise((resolve, reject) => {
39
+ let timer = null;
40
+ const check = () => {
41
+ try {
42
+ if (java.available !== false) {
43
+ if (timer)
44
+ clearTimeout(timer);
45
+ resolve();
46
+ return;
47
+ }
48
+ }
49
+ catch { }
50
+ if (Date.now() >= deadline) {
51
+ reject(new Error("Timed out waiting for Frida Java Bridge"));
52
+ return;
53
+ }
54
+ timer = setTimeout(check, intervalMs);
55
+ };
56
+ check();
57
+ });
58
+ }
59
+ function describeError(error) {
60
+ return error instanceof Error ? error.message : String(error);
61
+ }
62
+ export function runWhenJavaReady(java, operation, timeoutMs = 30000, label = "Java operation") {
63
+ return new Promise((resolve, reject) => {
64
+ let settled = false;
65
+ const timer = setTimeout(() => {
66
+ if (settled)
67
+ return;
68
+ settled = true;
69
+ reject(new Error(`Timed out waiting for ${label}`));
70
+ }, timeoutMs);
71
+ const finish = (callback) => {
72
+ if (settled)
73
+ return;
74
+ settled = true;
75
+ clearTimeout(timer);
76
+ callback();
77
+ };
78
+ const run = () => {
79
+ try {
80
+ const value = operation();
81
+ finish(() => resolve(value));
82
+ }
83
+ catch (error) {
84
+ finish(() => reject(error));
85
+ }
86
+ };
87
+ try {
88
+ performImmediately(java, run);
89
+ }
90
+ catch (error) {
91
+ finish(() => reject(error));
92
+ }
93
+ });
94
+ }
95
+ export function scheduleOnMainThread(java, operation, timeoutMs = 30000, label = "Android main thread") {
96
+ if (!java.scheduleOnMainThread) {
97
+ return Promise.reject(new Error("Java.scheduleOnMainThread is unavailable"));
98
+ }
99
+ return new Promise((resolve, reject) => {
100
+ let settled = false;
101
+ const timer = setTimeout(() => {
102
+ if (settled)
103
+ return;
104
+ settled = true;
105
+ reject(new Error(`Timed out waiting for Android main thread: ${label}`));
106
+ }, timeoutMs);
107
+ const finish = (callback) => {
108
+ if (settled)
109
+ return;
110
+ settled = true;
111
+ clearTimeout(timer);
112
+ callback();
113
+ };
114
+ try {
115
+ java.scheduleOnMainThread(() => {
116
+ try {
117
+ const value = operation();
118
+ finish(() => resolve(value));
119
+ }
120
+ catch (error) {
121
+ finish(() => reject(error));
122
+ }
123
+ });
124
+ }
125
+ catch (error) {
126
+ finish(() => reject(error));
127
+ }
128
+ });
129
+ }
130
+ function readNumber(value) {
131
+ const number = Number(value?.value ?? value);
132
+ return Number.isFinite(number) ? number : Number.NaN;
133
+ }
134
+ function tryCall(callback) {
135
+ try {
136
+ return callback() ?? null;
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ }
142
+ function getApplication(java) {
143
+ const activityThread = tryCall(() => java.use("android.app.ActivityThread"));
144
+ const current = activityThread
145
+ ? tryCall(() => activityThread.currentApplication())
146
+ : null;
147
+ if (current)
148
+ return current;
149
+ const appGlobals = tryCall(() => java.use("android.app.AppGlobals"));
150
+ const initial = appGlobals
151
+ ? tryCall(() => appGlobals.getInitialApplication())
152
+ : null;
153
+ if (initial)
154
+ return initial;
155
+ const thread = activityThread
156
+ ? tryCall(() => activityThread.currentActivityThread())
157
+ : null;
158
+ return thread ? tryCall(() => thread.getApplication()) : null;
159
+ }
160
+ export function getApplicationContext(java) {
161
+ const application = getApplication(java);
162
+ if (!application) {
163
+ throw new Error("Android Application is not ready. Call FloatMenu.waitForReady() before constructing the menu.");
164
+ }
165
+ const context = tryCall(() => application.getApplicationContext());
166
+ return context || application;
167
+ }
168
+ export function waitForApplicationContext(java, timeoutMs = 30000, intervalMs = 100) {
169
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
170
+ return Promise.reject(new RangeError("timeoutMs must be greater than zero"));
171
+ }
172
+ const deadline = Date.now() + timeoutMs;
173
+ return new Promise((resolve, reject) => {
174
+ let settled = false;
175
+ let lastError = null;
176
+ let retryTimer = null;
177
+ const hardTimeout = setTimeout(() => {
178
+ if (settled)
179
+ return;
180
+ settled = true;
181
+ if (retryTimer)
182
+ clearTimeout(retryTimer);
183
+ const suffix = lastError == null ? "" : `: ${describeError(lastError)}`;
184
+ reject(new Error(`Timed out waiting for Android Application context${suffix}`));
185
+ }, timeoutMs);
186
+ const finish = (callback) => {
187
+ if (settled)
188
+ return;
189
+ settled = true;
190
+ clearTimeout(hardTimeout);
191
+ if (retryTimer)
192
+ clearTimeout(retryTimer);
193
+ callback();
194
+ };
195
+ const check = () => {
196
+ const run = () => {
197
+ try {
198
+ const context = getApplicationContext(java);
199
+ finish(() => resolve(context));
200
+ }
201
+ catch (error) {
202
+ lastError = error;
203
+ if (Date.now() >= deadline) {
204
+ finish(() => reject(new Error(`Timed out waiting for Android Application context: ${describeError(error)}`)));
205
+ return;
206
+ }
207
+ retryTimer = setTimeout(check, intervalMs);
208
+ }
209
+ };
210
+ try {
211
+ performImmediately(java, run);
212
+ }
213
+ catch (error) {
214
+ lastError = error;
215
+ if (Date.now() >= deadline) {
216
+ finish(() => reject(new Error(`Timed out waiting for Android Application context: ${describeError(error)}`)));
217
+ return;
218
+ }
219
+ retryTimer = setTimeout(check, intervalMs);
220
+ }
221
+ };
222
+ check();
223
+ });
224
+ }
225
+ function hasWindowManagerMethods(service) {
226
+ return service != null
227
+ && typeof service.addView === "function"
228
+ && typeof service.updateViewLayout === "function"
229
+ && typeof service.removeView === "function";
230
+ }
231
+ export function getWindowManager(java, context) {
232
+ const service = tryCall(() => context.getSystemService("window"));
233
+ if (!service)
234
+ throw new Error("Android WindowManager service is unavailable");
235
+ if (java.cast) {
236
+ for (const className of ["android.view.WindowManager", "android.view.ViewManager"]) {
237
+ const klass = tryCall(() => java.use(className));
238
+ const casted = klass ? tryCall(() => java.cast(service, klass)) : null;
239
+ if (hasWindowManagerMethods(casted))
240
+ return casted;
241
+ }
242
+ }
243
+ if (hasWindowManagerMethods(service))
244
+ return service;
245
+ throw new Error("Android WindowManager service cannot be used as a ViewManager");
246
+ }
247
+ function readSize(metrics) {
248
+ const width = readNumber(metrics?.widthPixels);
249
+ const height = readNumber(metrics?.heightPixels);
250
+ if (width <= 0 || height <= 0 || width > 32768 || height > 32768)
251
+ return null;
252
+ return { width: Math.round(width), height: Math.round(height) };
253
+ }
254
+ export function readDisplaySize(context, windowManager) {
255
+ const metrics = tryCall(() => context.getResources().getDisplayMetrics());
256
+ const resourceSize = readSize(metrics);
257
+ if (resourceSize)
258
+ return resourceSize;
259
+ if (metrics) {
260
+ const display = tryCall(() => windowManager.getDefaultDisplay());
261
+ if (display) {
262
+ tryCall(() => display.getRealMetrics(metrics));
263
+ const realSize = readSize(metrics);
264
+ if (realSize)
265
+ return realSize;
266
+ tryCall(() => display.getMetrics(metrics));
267
+ const displaySize = readSize(metrics);
268
+ if (displaySize)
269
+ return displaySize;
270
+ }
271
+ }
272
+ throw new Error("Android display metrics are invalid or unavailable");
273
+ }
274
+ export function readDisplayDensity(context) {
275
+ const metrics = tryCall(() => context.getResources().getDisplayMetrics());
276
+ const density = readNumber(metrics?.density);
277
+ return density >= 0.25 && density <= 8 ? density : 1;
278
+ }
279
+ export function getOverlayWindowType(layoutParams) {
280
+ for (const fieldName of [
281
+ "TYPE_APPLICATION_OVERLAY",
282
+ "TYPE_PHONE",
283
+ "TYPE_SYSTEM_ALERT",
284
+ ]) {
285
+ const type = readNumber(layoutParams?.[fieldName]);
286
+ if (Number.isInteger(type) && type > 0)
287
+ return type;
288
+ }
289
+ throw new Error("No supported Android overlay window type is available");
290
+ }
291
+ export function ensureWindowNotFocusable(params, layoutParams) {
292
+ if (!params)
293
+ throw new Error("Window layout params are unavailable");
294
+ const flag = readNumber(layoutParams?.FLAG_NOT_FOCUSABLE);
295
+ if (!Number.isInteger(flag) || flag <= 0) {
296
+ throw new Error("FLAG_NOT_FOCUSABLE is unavailable");
297
+ }
298
+ const current = readNumber(params.flags);
299
+ const normalizedCurrent = Number.isInteger(current) ? current : 0;
300
+ const next = normalizedCurrent | flag;
301
+ if (next === normalizedCurrent)
302
+ return false;
303
+ if (params.flags && typeof params.flags === "object" && "value" in params.flags) {
304
+ params.flags.value = next;
305
+ }
306
+ else {
307
+ params.flags = next;
308
+ }
309
+ return true;
310
+ }
package/lib/api.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const API: Record<string, any>;