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.
- package/README.md +181 -0
- package/lib/android-listener.d.ts +20 -0
- package/lib/android-listener.js +92 -0
- package/lib/android-runtime.d.ts +25 -0
- package/lib/android-runtime.js +310 -0
- package/lib/api.d.ts +1 -0
- package/lib/api.js +156 -0
- package/lib/component/button.d.ts +11 -0
- package/lib/component/button.js +61 -0
- package/lib/component/category.d.ts +9 -0
- package/lib/component/category.js +57 -0
- package/lib/component/checkBox.d.ts +33 -0
- package/lib/component/checkBox.js +276 -0
- package/lib/component/collapsible.d.ts +19 -0
- package/lib/component/collapsible.js +197 -0
- package/lib/component/image.d.ts +18 -0
- package/lib/component/image.js +85 -0
- package/lib/component/infoCardBox.d.ts +13 -0
- package/lib/component/infoCardBox.js +68 -0
- package/lib/component/input.d.ts +41 -0
- package/lib/component/input.js +342 -0
- package/lib/component/selector.d.ts +31 -0
- package/lib/component/selector.js +246 -0
- package/lib/component/slider.d.ts +18 -0
- package/lib/component/slider.js +156 -0
- package/lib/component/style/style.d.ts +4 -0
- package/lib/component/style/style.js +325 -0
- package/lib/component/style/theme.d.ts +33 -0
- package/lib/component/style/theme.js +24 -0
- package/lib/component/switch.d.ts +12 -0
- package/lib/component/switch.js +117 -0
- package/lib/component/text.d.ts +9 -0
- package/lib/component/text.js +36 -0
- package/lib/component/ui-components.d.ts +34 -0
- package/lib/component/ui-components.js +108 -0
- package/lib/component/views/header-view.d.ts +20 -0
- package/lib/component/views/header-view.js +198 -0
- package/lib/component/views/log-view.d.ts +41 -0
- package/lib/component/views/log-view.js +511 -0
- package/lib/component/views/tabs-view.d.ts +41 -0
- package/lib/component/views/tabs-view.js +334 -0
- package/lib/constant-config.d.ts +5 -0
- package/lib/constant-config.js +3 -0
- package/lib/event-emitter.d.ts +10 -0
- package/lib/event-emitter.js +52 -0
- package/lib/float-menu.d.ts +103 -0
- package/lib/float-menu.js +648 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +16 -0
- package/lib/java-runtime.d.ts +2 -0
- package/lib/java-runtime.js +2 -0
- package/lib/logger.d.ts +43 -0
- package/lib/logger.js +200 -0
- package/lib/runtime/component-registry.d.ts +24 -0
- package/lib/runtime/component-registry.js +48 -0
- package/lib/runtime/launch-sequence.d.ts +5 -0
- package/lib/runtime/launch-sequence.js +20 -0
- package/lib/runtime/lifecycle-controller.d.ts +9 -0
- package/lib/runtime/lifecycle-controller.js +31 -0
- package/lib/runtime/listener-registry.d.ts +7 -0
- package/lib/runtime/listener-registry.js +40 -0
- package/lib/runtime/overlay-options.d.ts +9 -0
- package/lib/runtime/overlay-options.js +42 -0
- package/lib/runtime/safe-runtime.d.ts +3 -0
- package/lib/runtime/safe-runtime.js +20 -0
- package/lib/utils.d.ts +8 -0
- package/lib/utils.js +13 -0
- package/package.json +43 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import { EventEmitter } from "./event-emitter.js";
|
|
2
|
+
import { Logger } from "./logger.js";
|
|
3
|
+
import { API } from "./api.js";
|
|
4
|
+
import { applyStyle, dp } from "./component/style/style.js";
|
|
5
|
+
import { DarkNeonTheme } from "./component/style/theme.js";
|
|
6
|
+
import { logicalToWindow, windowToLogical } from "./utils.js";
|
|
7
|
+
import { TabsView } from "./component/views/tabs-view.js";
|
|
8
|
+
import { HeaderView } from "./component/views/header-view.js";
|
|
9
|
+
import { ConstantConfig } from "./constant-config.js";
|
|
10
|
+
import { assertJavaBridgeCompatible, ensureWindowNotFocusable, getApplicationContext, getOverlayWindowType, getWindowManager, readDisplaySize, runWhenJavaReady, scheduleOnMainThread, waitForJavaAvailable, waitForApplicationContext, } from "./android-runtime.js";
|
|
11
|
+
import { ComponentRegistry } from "./runtime/component-registry.js";
|
|
12
|
+
import { LifecycleController, } from "./runtime/lifecycle-controller.js";
|
|
13
|
+
import { ListenerRegistry } from "./runtime/listener-registry.js";
|
|
14
|
+
import Java from "./java-runtime.js";
|
|
15
|
+
import { createJavaListener } from "./android-listener.js";
|
|
16
|
+
import { runLaunchSequence } from "./runtime/launch-sequence.js";
|
|
17
|
+
import { DEFAULT_PERMISSION_DENIED_MESSAGE, InvalidOverlayIconError, normalizeIconBase64, requireOverlayPermission, } from "./runtime/overlay-options.js";
|
|
18
|
+
const HIDDEN_HOTSPOT_MESSAGE = "菜单已隐藏,点击原悬浮图标位置可重新打开";
|
|
19
|
+
export class FloatMenu {
|
|
20
|
+
static async waitForReady(timeoutMs = 30000) {
|
|
21
|
+
assertJavaBridgeCompatible(Java);
|
|
22
|
+
const startedAt = Date.now();
|
|
23
|
+
await waitForJavaAvailable(Java, timeoutMs);
|
|
24
|
+
const remaining = Math.max(1, timeoutMs - (Date.now() - startedAt));
|
|
25
|
+
return waitForApplicationContext(Java, remaining);
|
|
26
|
+
}
|
|
27
|
+
static async create(options = {}, timeoutMs = 30000) {
|
|
28
|
+
await FloatMenu.waitForReady(timeoutMs);
|
|
29
|
+
return runWhenJavaReady(Java, () => new FloatMenu(options), timeoutMs, "FloatMenu creation");
|
|
30
|
+
}
|
|
31
|
+
static launch(options = {}, setup = () => { }, timeoutMs = 30000) {
|
|
32
|
+
return runLaunchSequence(() => FloatMenu.create(options, timeoutMs), setup, error => Logger.instance.warn("FloatMenu launch cleanup failed:", error));
|
|
33
|
+
}
|
|
34
|
+
get context() {
|
|
35
|
+
if (this._context === null) {
|
|
36
|
+
this._context = getApplicationContext(Java);
|
|
37
|
+
}
|
|
38
|
+
return this._context;
|
|
39
|
+
}
|
|
40
|
+
get windowManager() {
|
|
41
|
+
if (this._windowManager === null) {
|
|
42
|
+
this._windowManager = getWindowManager(Java, this.context);
|
|
43
|
+
}
|
|
44
|
+
return this._windowManager;
|
|
45
|
+
}
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
this.components = new ComponentRegistry();
|
|
48
|
+
this.componentListeners = new ListenerRegistry();
|
|
49
|
+
this.lifecycle = new LifecycleController();
|
|
50
|
+
this.pendingComponents = [];
|
|
51
|
+
this.eventEmitter = new EventEmitter();
|
|
52
|
+
this.isIconMode = true;
|
|
53
|
+
this._context = null;
|
|
54
|
+
this.iconAttached = false;
|
|
55
|
+
this.menuAttached = false;
|
|
56
|
+
this.lastPresentation = "icon";
|
|
57
|
+
this._windowManager = null;
|
|
58
|
+
const iconWidth = options.icon?.width ?? 200;
|
|
59
|
+
const iconHeight = options.icon?.height ?? 200;
|
|
60
|
+
if (!Number.isFinite(iconWidth) || iconWidth <= 0) {
|
|
61
|
+
throw new RangeError("Overlay icon width must be greater than zero");
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isFinite(iconHeight) || iconHeight <= 0) {
|
|
64
|
+
throw new RangeError("Overlay icon height must be greater than zero");
|
|
65
|
+
}
|
|
66
|
+
const icon = options.icon
|
|
67
|
+
? {
|
|
68
|
+
base64: normalizeIconBase64(options.icon.base64),
|
|
69
|
+
width: Math.round(iconWidth),
|
|
70
|
+
height: Math.round(iconHeight),
|
|
71
|
+
}
|
|
72
|
+
: undefined;
|
|
73
|
+
this.options = {
|
|
74
|
+
width: 1200,
|
|
75
|
+
height: 1400,
|
|
76
|
+
x: 0,
|
|
77
|
+
y: 0,
|
|
78
|
+
logMaxLines: 100,
|
|
79
|
+
version: "v1.0.0",
|
|
80
|
+
title: "Frida Float Menu",
|
|
81
|
+
theme: DarkNeonTheme,
|
|
82
|
+
tabs: [],
|
|
83
|
+
activeTab: undefined,
|
|
84
|
+
...options,
|
|
85
|
+
icon,
|
|
86
|
+
permissionDeniedMessage: options.permissionDeniedMessage ?? DEFAULT_PERMISSION_DENIED_MESSAGE,
|
|
87
|
+
};
|
|
88
|
+
this.iconWidth = Math.round(iconWidth);
|
|
89
|
+
this.iconHeight = Math.round(iconHeight);
|
|
90
|
+
this.logger = Logger.instance;
|
|
91
|
+
const display = readDisplaySize(this.context, this.windowManager);
|
|
92
|
+
ConstantConfig.screenWidth = display.width;
|
|
93
|
+
ConstantConfig.screenHeight = display.height;
|
|
94
|
+
this.screenWidth = display.width;
|
|
95
|
+
this.screenHeight = display.height;
|
|
96
|
+
this.options.width = Math.min(this.options.width, display.width);
|
|
97
|
+
this.options.height = Math.min(this.options.height, Math.max(1, display.height - 80));
|
|
98
|
+
this.headerComponent = new HeaderView(this.options.theme);
|
|
99
|
+
this.tabsView = new TabsView(this.context, this.options.theme, this.options.tabs, this.options.activeTab);
|
|
100
|
+
this.tabsView.initializeTabs();
|
|
101
|
+
}
|
|
102
|
+
get state() {
|
|
103
|
+
return this.lifecycle.state;
|
|
104
|
+
}
|
|
105
|
+
runOnMainThread(label, operation) {
|
|
106
|
+
return scheduleOnMainThread(Java, operation, 30000, label);
|
|
107
|
+
}
|
|
108
|
+
reportAsyncFailure(label, error) {
|
|
109
|
+
this.logger.error(`${label}:`, error);
|
|
110
|
+
}
|
|
111
|
+
enforceNonFocusableWindow(window, params, attached) {
|
|
112
|
+
const changed = ensureWindowNotFocusable(params, API.LayoutParams);
|
|
113
|
+
try {
|
|
114
|
+
window?.clearFocus?.();
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
if (changed && attached) {
|
|
118
|
+
this.windowManager.updateViewLayout(window, params);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
addDragListener(targetView, window, winParams, isShowing) {
|
|
122
|
+
const OnTouchListener = API.OnTouchListener;
|
|
123
|
+
const MotionEvent = API.MotionEvent;
|
|
124
|
+
targetView.setClickable(true);
|
|
125
|
+
const getBounds = () => {
|
|
126
|
+
const w = this.isIconMode ? this.iconWidth : this.options.width;
|
|
127
|
+
const h = this.isIconMode
|
|
128
|
+
? this.iconHeight
|
|
129
|
+
: this.options.height;
|
|
130
|
+
return {
|
|
131
|
+
left: 0,
|
|
132
|
+
top: -40,
|
|
133
|
+
right: this.screenWidth - w,
|
|
134
|
+
bottom: this.screenHeight - h,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const self = this;
|
|
138
|
+
let isDragging = false;
|
|
139
|
+
const DRAG_THRESHOLD = 5;
|
|
140
|
+
let touchOffsetX = 0;
|
|
141
|
+
let touchOffsetY = 0;
|
|
142
|
+
const touchListener = createJavaListener({
|
|
143
|
+
key: "view-touch",
|
|
144
|
+
interfaceClass: OnTouchListener,
|
|
145
|
+
fallback: { onTouch: false },
|
|
146
|
+
callbacks: {
|
|
147
|
+
onTouch: (_v, event) => {
|
|
148
|
+
const action = event.getAction();
|
|
149
|
+
switch (action) {
|
|
150
|
+
case MotionEvent.ACTION_DOWN.value: {
|
|
151
|
+
isDragging = false;
|
|
152
|
+
const rawX = event.getRawX();
|
|
153
|
+
const rawY = event.getRawY();
|
|
154
|
+
const startWx = winParams.x.value;
|
|
155
|
+
const startWy = winParams.y.value;
|
|
156
|
+
touchOffsetX = rawX - startWx;
|
|
157
|
+
touchOffsetY = rawY - startWy;
|
|
158
|
+
self.lastTouchX = rawX;
|
|
159
|
+
self.lastTouchY = rawY;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
case MotionEvent.ACTION_MOVE.value: {
|
|
163
|
+
const rawX = event.getRawX();
|
|
164
|
+
const rawY = event.getRawY();
|
|
165
|
+
const dx = rawX - self.lastTouchX;
|
|
166
|
+
const dy = rawY - self.lastTouchY;
|
|
167
|
+
if (!isDragging &&
|
|
168
|
+
(Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) {
|
|
169
|
+
isDragging = true;
|
|
170
|
+
}
|
|
171
|
+
if (isDragging) {
|
|
172
|
+
let wx = rawX - touchOffsetX;
|
|
173
|
+
let wy = rawY - touchOffsetY;
|
|
174
|
+
const p = windowToLogical(wx, wy, self.isIconMode
|
|
175
|
+
? self.iconWidth
|
|
176
|
+
: self.options.width, self.isIconMode
|
|
177
|
+
? self.iconHeight
|
|
178
|
+
: self.options.height);
|
|
179
|
+
let newX = p.x;
|
|
180
|
+
let newY = p.y;
|
|
181
|
+
const bounds = getBounds();
|
|
182
|
+
newX = Math.max(bounds.left, Math.min(bounds.right, newX));
|
|
183
|
+
newY = Math.max(bounds.top, Math.min(bounds.bottom, newY));
|
|
184
|
+
if (isShowing()) {
|
|
185
|
+
self.updatePosition(window, winParams, { x: newX, y: newY });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
case MotionEvent.ACTION_UP.value:
|
|
191
|
+
case MotionEvent.ACTION_CANCEL.value: {
|
|
192
|
+
if (!isDragging) {
|
|
193
|
+
try {
|
|
194
|
+
self.isIconMode = false;
|
|
195
|
+
self.iconContainerWin.setAlpha(1);
|
|
196
|
+
void self.present("menu").catch((error) => self.reportAsyncFailure("Failed to open menu", error));
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
this.componentListeners.add("java:menu", () => touchListener.dispose());
|
|
208
|
+
targetView.setOnTouchListener(touchListener.instance);
|
|
209
|
+
}
|
|
210
|
+
createMenuContainerWindow() {
|
|
211
|
+
const FrameLayout = API.FrameLayout;
|
|
212
|
+
const LinearLayout = API.LinearLayout;
|
|
213
|
+
const FrameLayoutParams = API.FrameLayoutParams;
|
|
214
|
+
const ViewGroupLayoutParams = API.ViewGroupLayoutParams;
|
|
215
|
+
const View = API.View;
|
|
216
|
+
const LayoutParams = API.LayoutParams;
|
|
217
|
+
this.menuContainerWin = FrameLayout.$new(this.context);
|
|
218
|
+
const rootLp = FrameLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.MATCH_PARENT.value);
|
|
219
|
+
rootLp.gravity = API.Gravity.TOP.value | API.Gravity.START.value;
|
|
220
|
+
this.menuContainerWin.setLayoutParams(rootLp);
|
|
221
|
+
try {
|
|
222
|
+
this.menuContainerWin.setBackgroundColor(0x00000000);
|
|
223
|
+
}
|
|
224
|
+
catch (e) { }
|
|
225
|
+
const panel = LinearLayout.$new(this.context);
|
|
226
|
+
panel.setOrientation(LinearLayout.VERTICAL.value);
|
|
227
|
+
panel.setLayoutParams(ViewGroupLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.MATCH_PARENT.value));
|
|
228
|
+
applyStyle(panel, "overlay", this.options.theme);
|
|
229
|
+
try {
|
|
230
|
+
panel.setClipToOutline(true);
|
|
231
|
+
}
|
|
232
|
+
catch (e) { }
|
|
233
|
+
try {
|
|
234
|
+
panel.setClipChildren(false);
|
|
235
|
+
}
|
|
236
|
+
catch (e) { }
|
|
237
|
+
try {
|
|
238
|
+
panel.setClipToPadding(false);
|
|
239
|
+
}
|
|
240
|
+
catch (e) { }
|
|
241
|
+
this.menuPanelView = panel;
|
|
242
|
+
this.menuContainerWin.addView(panel);
|
|
243
|
+
this.menuWindowParams = LayoutParams.$new(this.options.width, this.options.height, 0, 0, getOverlayWindowType(LayoutParams), LayoutParams.FLAG_NOT_FOCUSABLE.value |
|
|
244
|
+
LayoutParams.FLAG_NOT_TOUCH_MODAL.value, 1);
|
|
245
|
+
this.headerView = this.headerComponent.createView({
|
|
246
|
+
context: this.context,
|
|
247
|
+
parent: this.menuPanelView,
|
|
248
|
+
logMaxLines: this.options.logMaxLines,
|
|
249
|
+
title: this.options.title,
|
|
250
|
+
version: this.options.version,
|
|
251
|
+
}, {
|
|
252
|
+
onMinimize: () => {
|
|
253
|
+
void this.present("icon").catch((error) => this.reportAsyncFailure("Failed to minimize menu", error));
|
|
254
|
+
},
|
|
255
|
+
onHide: () => {
|
|
256
|
+
void this.concealFromHeader();
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
if (this.headerView) {
|
|
260
|
+
this.addDragListener(this.headerView, this.menuContainerWin, this.menuWindowParams, () => !this.isIconMode);
|
|
261
|
+
}
|
|
262
|
+
this.tabsView.createTabView(this.menuPanelView);
|
|
263
|
+
this.tabsView.createTabContainer();
|
|
264
|
+
this.windowManager.addView(this.menuContainerWin, this.menuWindowParams);
|
|
265
|
+
this.menuAttached = true;
|
|
266
|
+
this.menuContainerWin.setVisibility(View.GONE.value);
|
|
267
|
+
}
|
|
268
|
+
updatePosition(window, winParams, newPos) {
|
|
269
|
+
const { x: wx, y: wy } = logicalToWindow(newPos.x, newPos.y, this.isIconMode ? this.iconWidth : this.options.width, this.isIconMode ? this.iconHeight : this.options.height);
|
|
270
|
+
winParams.x.value = wx | 0;
|
|
271
|
+
winParams.y.value = wy | 0;
|
|
272
|
+
Java.scheduleOnMainThread(() => {
|
|
273
|
+
this.enforceNonFocusableWindow(window, winParams, true);
|
|
274
|
+
this.windowManager.updateViewLayout(window, winParams);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
createIconWindow() {
|
|
278
|
+
const ImageView = API.ImageView;
|
|
279
|
+
const ImageView$ScaleType = API.ImageViewScaleType;
|
|
280
|
+
const FrameLayoutParams = API.FrameLayoutParams;
|
|
281
|
+
const Gravity = API.Gravity;
|
|
282
|
+
const LayoutParams = API.LayoutParams;
|
|
283
|
+
const BitmapFactory = API.BitmapFactory;
|
|
284
|
+
const Base64 = API.Base64;
|
|
285
|
+
const FrameLayout = API.FrameLayout;
|
|
286
|
+
this.iconView = ImageView.$new(this.context);
|
|
287
|
+
if (this.options.icon) {
|
|
288
|
+
try {
|
|
289
|
+
const decoded = Base64.decode(this.options.icon.base64, Base64.DEFAULT.value);
|
|
290
|
+
if (!decoded || decoded.length <= 0) {
|
|
291
|
+
throw new InvalidOverlayIconError("Overlay icon decoded to an empty byte array");
|
|
292
|
+
}
|
|
293
|
+
const bitmap = BitmapFactory.decodeByteArray(decoded, 0, decoded.length);
|
|
294
|
+
if (!bitmap) {
|
|
295
|
+
throw new InvalidOverlayIconError("Overlay icon Base64 is not a supported bitmap");
|
|
296
|
+
}
|
|
297
|
+
this.iconView.setImageBitmap(bitmap);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
if (error instanceof InvalidOverlayIconError)
|
|
301
|
+
throw error;
|
|
302
|
+
throw new InvalidOverlayIconError(`Failed to decode overlay icon: ${String(error)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
this.iconView.setBackgroundColor(0xff4285f4 | 0);
|
|
307
|
+
try {
|
|
308
|
+
this.iconView.setClipToOutline(true);
|
|
309
|
+
}
|
|
310
|
+
catch { }
|
|
311
|
+
}
|
|
312
|
+
this.iconView.setScaleType(ImageView$ScaleType.FIT_CENTER.value);
|
|
313
|
+
const { x, y } = logicalToWindow(this.options.x, this.options.y, this.iconWidth, this.iconHeight);
|
|
314
|
+
this.iconWindowParams = LayoutParams.$new(this.iconWidth, this.iconHeight, x, y, getOverlayWindowType(LayoutParams), LayoutParams.FLAG_NOT_FOCUSABLE.value |
|
|
315
|
+
LayoutParams.FLAG_NOT_TOUCH_MODAL.value, 1);
|
|
316
|
+
this.iconContainerWin = FrameLayout.$new(this.context);
|
|
317
|
+
this.iconContainerWin.setLayoutParams(FrameLayoutParams.$new(this.iconWidth, this.iconHeight, Gravity.CENTER.value));
|
|
318
|
+
this.iconContainerWin.addView(this.iconView);
|
|
319
|
+
this.windowManager.addView(this.iconContainerWin, this.iconWindowParams);
|
|
320
|
+
this.iconAttached = true;
|
|
321
|
+
this.addDragListener(this.iconContainerWin, this.iconContainerWin, this.iconWindowParams, () => this.isIconMode);
|
|
322
|
+
}
|
|
323
|
+
canDrawOverlays() {
|
|
324
|
+
try {
|
|
325
|
+
const Settings = Java.use("android.provider.Settings");
|
|
326
|
+
return Boolean(Settings.canDrawOverlays(this.context));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
Logger.instance.warn("Unable to query overlay permission; addView will perform the final check:", error);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
showToastNow(message, duration = 0) {
|
|
334
|
+
const Toast = Java.use("android.widget.Toast");
|
|
335
|
+
const JString = Java.use("java.lang.String");
|
|
336
|
+
Toast.makeText(this.context, JString.$new(message), duration).show();
|
|
337
|
+
}
|
|
338
|
+
async concealFromHeader() {
|
|
339
|
+
try {
|
|
340
|
+
await this.conceal();
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
this.reportAsyncFailure("Failed to hide menu", error);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
await this.toast(HIDDEN_HOTSPOT_MESSAGE);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
this.reportAsyncFailure("Failed to show hidden hotspot guidance", error);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async mountInternal() {
|
|
354
|
+
if (this.iconAttached && this.menuAttached)
|
|
355
|
+
return;
|
|
356
|
+
await this.runOnMainThread("mount floating windows", () => {
|
|
357
|
+
requireOverlayPermission(this.canDrawOverlays(), this.options.permissionDeniedMessage, message => this.showToastNow(message), error => this.logger.warn("Failed to show overlay permission Toast:", error));
|
|
358
|
+
try {
|
|
359
|
+
if (!this.iconAttached)
|
|
360
|
+
this.createIconWindow();
|
|
361
|
+
if (!this.menuAttached)
|
|
362
|
+
this.createMenuContainerWindow();
|
|
363
|
+
this.enforceNonFocusableWindow(this.iconContainerWin, this.iconWindowParams, this.iconAttached);
|
|
364
|
+
this.enforceNonFocusableWindow(this.menuContainerWin, this.menuWindowParams, this.menuAttached);
|
|
365
|
+
this.processPendingComponents(this.context);
|
|
366
|
+
const View = API.View;
|
|
367
|
+
this.iconContainerWin.setVisibility(View.GONE.value);
|
|
368
|
+
this.menuContainerWin.setVisibility(View.GONE.value);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
this.detachWindows();
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
this.lifecycle.transition("mounted");
|
|
376
|
+
}
|
|
377
|
+
async presentInternal(mode) {
|
|
378
|
+
await this.mountInternal();
|
|
379
|
+
await this.runOnMainThread(`present ${mode}`, () => {
|
|
380
|
+
const View = API.View;
|
|
381
|
+
this.isIconMode = mode === "icon";
|
|
382
|
+
this.lastPresentation = mode;
|
|
383
|
+
this.enforceNonFocusableWindow(this.iconContainerWin, this.iconWindowParams, this.iconAttached);
|
|
384
|
+
this.enforceNonFocusableWindow(this.menuContainerWin, this.menuWindowParams, this.menuAttached);
|
|
385
|
+
this.iconContainerWin.setAlpha(1);
|
|
386
|
+
this.iconContainerWin.setVisibility(mode === "icon" ? View.VISIBLE.value : View.GONE.value);
|
|
387
|
+
this.menuContainerWin.setVisibility(mode === "menu" ? View.VISIBLE.value : View.GONE.value);
|
|
388
|
+
});
|
|
389
|
+
this.lifecycle.transition(mode === "icon" ? "icon-visible" : "menu-visible");
|
|
390
|
+
}
|
|
391
|
+
mount() {
|
|
392
|
+
return this.lifecycle.run("mount", () => this.mountInternal());
|
|
393
|
+
}
|
|
394
|
+
present(mode = this.lastPresentation) {
|
|
395
|
+
return this.lifecycle.run(`present:${mode}`, () => this.presentInternal(mode));
|
|
396
|
+
}
|
|
397
|
+
conceal() {
|
|
398
|
+
return this.lifecycle.run("conceal", async () => {
|
|
399
|
+
if (this.iconAttached || this.menuAttached) {
|
|
400
|
+
await this.runOnMainThread("conceal floating windows", () => {
|
|
401
|
+
const View = API.View;
|
|
402
|
+
if (this.iconAttached) {
|
|
403
|
+
this.enforceNonFocusableWindow(this.iconContainerWin, this.iconWindowParams, true);
|
|
404
|
+
this.isIconMode = true;
|
|
405
|
+
this.iconContainerWin.setAlpha(0);
|
|
406
|
+
this.iconContainerWin.setVisibility(View.VISIBLE.value);
|
|
407
|
+
}
|
|
408
|
+
if (this.menuAttached) {
|
|
409
|
+
this.enforceNonFocusableWindow(this.menuContainerWin, this.menuWindowParams, true);
|
|
410
|
+
this.menuContainerWin.setVisibility(View.GONE.value);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
this.lifecycle.transition("hidden");
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
toggle() {
|
|
418
|
+
return this.lifecycle.run("toggle", () => {
|
|
419
|
+
const mode = this.lifecycle.state === "menu-visible"
|
|
420
|
+
? "icon"
|
|
421
|
+
: this.lifecycle.state === "icon-visible"
|
|
422
|
+
? "menu"
|
|
423
|
+
: this.lastPresentation;
|
|
424
|
+
return this.presentInternal(mode);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
detachWindows() {
|
|
428
|
+
if (this.menuContainerWin && this.menuAttached) {
|
|
429
|
+
try {
|
|
430
|
+
this.windowManager.removeView(this.menuContainerWin);
|
|
431
|
+
}
|
|
432
|
+
catch { }
|
|
433
|
+
}
|
|
434
|
+
if (this.iconContainerWin && this.iconAttached) {
|
|
435
|
+
try {
|
|
436
|
+
this.windowManager.removeView(this.iconContainerWin);
|
|
437
|
+
}
|
|
438
|
+
catch { }
|
|
439
|
+
}
|
|
440
|
+
this.menuAttached = false;
|
|
441
|
+
this.iconAttached = false;
|
|
442
|
+
}
|
|
443
|
+
bindComponentEvents(component) {
|
|
444
|
+
const id = component.getId();
|
|
445
|
+
const owner = `component:${id}`;
|
|
446
|
+
this.componentListeners.clear(owner);
|
|
447
|
+
this.componentListeners.add(owner, component.on("valueChanged", (value) => {
|
|
448
|
+
this.eventEmitter.emit("component:" + id + ":valueChanged", value);
|
|
449
|
+
}));
|
|
450
|
+
this.componentListeners.add(owner, component.on("action", (data) => {
|
|
451
|
+
this.eventEmitter.emit("component:" + id + ":action", data);
|
|
452
|
+
}));
|
|
453
|
+
this.componentListeners.add(owner, component.on("click", (data) => {
|
|
454
|
+
this.eventEmitter.emit("component:" + id + ":click", data);
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
processPendingComponents(context) {
|
|
458
|
+
if (this.pendingComponents.length === 0)
|
|
459
|
+
return;
|
|
460
|
+
for (const { id, component, tabId } of this.pendingComponents) {
|
|
461
|
+
try {
|
|
462
|
+
const tabInfo = this.tabsView.tabs.get(tabId);
|
|
463
|
+
if (!tabInfo) {
|
|
464
|
+
this.logger.error(`Cannot add pending component ${id} - tab ${tabId} not found`);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const view = this.prepareComponentView(context, component);
|
|
468
|
+
const container = tabInfo.container || this.tabsView.currentContentContainer;
|
|
469
|
+
if (!container)
|
|
470
|
+
throw new Error(`Tab ${tabId} has no container`);
|
|
471
|
+
container.addView(view);
|
|
472
|
+
component.attach(container);
|
|
473
|
+
this.components.attach(id, container);
|
|
474
|
+
tabInfo.components.add(id);
|
|
475
|
+
this.bindComponentEvents(component);
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
this.componentListeners.clear(`component:${id}`);
|
|
479
|
+
this.components.remove(id);
|
|
480
|
+
const tabInfo = this.tabsView.tabs.get(tabId);
|
|
481
|
+
tabInfo?.components.delete(id);
|
|
482
|
+
component.dispose();
|
|
483
|
+
Logger.instance.error(`Failed to add pending component ${id}: ` + error);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
this.pendingComponents = [];
|
|
487
|
+
}
|
|
488
|
+
prepareComponentView(context, component) {
|
|
489
|
+
const LinearLayoutParams = API.LinearLayoutParams;
|
|
490
|
+
const ViewGroupLayoutParams = API.ViewGroupLayoutParams;
|
|
491
|
+
const gapNormal = dp(context, 10);
|
|
492
|
+
component.initialize(context, this);
|
|
493
|
+
const view = component.getView();
|
|
494
|
+
const lp = LinearLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.WRAP_CONTENT.value);
|
|
495
|
+
lp.setMargins(0, 0, 0, gapNormal);
|
|
496
|
+
view.setLayoutParams(lp);
|
|
497
|
+
return view;
|
|
498
|
+
}
|
|
499
|
+
dispose() {
|
|
500
|
+
return this.lifecycle.run("dispose", async () => {
|
|
501
|
+
if (this.lifecycle.disposed)
|
|
502
|
+
return;
|
|
503
|
+
this.componentListeners.clearAll();
|
|
504
|
+
this.eventEmitter.removeAllListeners();
|
|
505
|
+
for (const { component } of this.components.values())
|
|
506
|
+
component.detach();
|
|
507
|
+
this.headerComponent.destroy();
|
|
508
|
+
this.tabsView.destroy();
|
|
509
|
+
await this.runOnMainThread("dispose floating windows", () => {
|
|
510
|
+
this.detachWindows();
|
|
511
|
+
});
|
|
512
|
+
this.lifecycle.transition("disposing");
|
|
513
|
+
for (const { component } of this.components.values()) {
|
|
514
|
+
await component.dispose();
|
|
515
|
+
}
|
|
516
|
+
this.iconView = null;
|
|
517
|
+
this.iconContainerWin = null;
|
|
518
|
+
this.iconWindowParams = null;
|
|
519
|
+
this.menuPanelView = null;
|
|
520
|
+
this.menuContainerWin = null;
|
|
521
|
+
this.menuWindowParams = null;
|
|
522
|
+
this.pendingComponents = [];
|
|
523
|
+
this.components.clear();
|
|
524
|
+
this.lifecycle.transition("disposed");
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
toast(msg, duration = 0) {
|
|
528
|
+
return this.runOnMainThread("show toast", () => this.showToastNow(msg, duration));
|
|
529
|
+
}
|
|
530
|
+
addComponent(component, tabId) {
|
|
531
|
+
const id = component.getId();
|
|
532
|
+
return this.lifecycle.run(`addComponent:${id}`, async () => {
|
|
533
|
+
const targetTabId = tabId || this.tabsView.activeTabId;
|
|
534
|
+
const tabInfo = this.tabsView.tabs.get(targetTabId);
|
|
535
|
+
if (!tabInfo) {
|
|
536
|
+
throw new Error(`Cannot add component ${id}: tab ${targetTabId} not found`);
|
|
537
|
+
}
|
|
538
|
+
this.components.add(component, targetTabId);
|
|
539
|
+
component.setMenu(this);
|
|
540
|
+
tabInfo.components.add(id);
|
|
541
|
+
if (!this.menuPanelView) {
|
|
542
|
+
this.pendingComponents.push({ id, component, tabId: targetTabId });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
await this.runOnMainThread(`add component ${id}`, () => {
|
|
547
|
+
const context = this.menuPanelView.getContext();
|
|
548
|
+
const view = this.prepareComponentView(context, component);
|
|
549
|
+
const container = tabInfo.container || this.tabsView.currentContentContainer;
|
|
550
|
+
if (!container)
|
|
551
|
+
throw new Error(`Tab ${targetTabId} has no container`);
|
|
552
|
+
container.addView(view);
|
|
553
|
+
component.attach(container);
|
|
554
|
+
this.components.attach(id, container);
|
|
555
|
+
this.bindComponentEvents(component);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
tabInfo.components.delete(id);
|
|
560
|
+
this.components.remove(id);
|
|
561
|
+
this.componentListeners.clear(`component:${id}`);
|
|
562
|
+
component.dispose();
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
addNestedComponent(ownerId, component, container) {
|
|
568
|
+
const id = component.getId();
|
|
569
|
+
return this.lifecycle.run(`addNestedComponent:${id}`, async () => {
|
|
570
|
+
await this.runOnMainThread(`add nested component ${id}`, () => this.attachNestedComponent(ownerId, component, container));
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
attachNestedComponent(ownerId, component, container) {
|
|
574
|
+
const id = component.getId();
|
|
575
|
+
const owner = this.components.getEntry(ownerId);
|
|
576
|
+
if (!owner)
|
|
577
|
+
throw new Error(`Parent component ${ownerId} is not registered`);
|
|
578
|
+
this.components.add(component, owner.tabId, ownerId);
|
|
579
|
+
component.setMenu(this);
|
|
580
|
+
try {
|
|
581
|
+
const view = this.prepareComponentView(container.getContext(), component);
|
|
582
|
+
container.addView(view);
|
|
583
|
+
component.attach(container);
|
|
584
|
+
this.components.attach(id, container);
|
|
585
|
+
this.bindComponentEvents(component);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
this.components.remove(id);
|
|
589
|
+
this.componentListeners.clear(`component:${id}`);
|
|
590
|
+
component.dispose();
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
removeComponent(id) {
|
|
595
|
+
return this.lifecycle.run(`removeComponent:${id}`, async () => {
|
|
596
|
+
const entry = this.components.getEntry(id);
|
|
597
|
+
if (!entry)
|
|
598
|
+
return;
|
|
599
|
+
const { component, tabId, container } = entry;
|
|
600
|
+
const tabInfo = this.tabsView.tabs.get(tabId);
|
|
601
|
+
this.pendingComponents = this.pendingComponents.filter((item) => item.id !== id);
|
|
602
|
+
this.componentListeners.clear(`component:${id}`);
|
|
603
|
+
if (component.isInitialized) {
|
|
604
|
+
await this.runOnMainThread(`remove component ${id}`, () => {
|
|
605
|
+
const view = component.getView();
|
|
606
|
+
const parent = container || tabInfo?.container || this.tabsView.currentContentContainer;
|
|
607
|
+
if (parent && view)
|
|
608
|
+
parent.removeView(view);
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
component.detach();
|
|
612
|
+
await component.dispose();
|
|
613
|
+
tabInfo?.components.delete(id);
|
|
614
|
+
this.components.remove(id);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
getComponent(id) {
|
|
618
|
+
return this.components.get(id);
|
|
619
|
+
}
|
|
620
|
+
setComponentValue(id, value) {
|
|
621
|
+
this.components.get(id)?.setValue(value);
|
|
622
|
+
}
|
|
623
|
+
captureState() {
|
|
624
|
+
return {
|
|
625
|
+
activeTabId: this.tabsView.activeTabId,
|
|
626
|
+
presentation: this.lastPresentation,
|
|
627
|
+
components: this.components.captureState(),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
restoreState(snapshot) {
|
|
631
|
+
return this.lifecycle.run("restoreState", async () => {
|
|
632
|
+
if (!this.tabsView.tabs.has(snapshot.activeTabId)) {
|
|
633
|
+
throw new Error(`Cannot restore unknown tab ${snapshot.activeTabId}`);
|
|
634
|
+
}
|
|
635
|
+
this.tabsView.selectTab(snapshot.activeTabId);
|
|
636
|
+
this.lastPresentation = snapshot.presentation;
|
|
637
|
+
for (const [id, state] of Object.entries(snapshot.components)) {
|
|
638
|
+
this.components.get(id)?.restoreState(state);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
on(event, callback) {
|
|
643
|
+
return this.eventEmitter.on(event, callback);
|
|
644
|
+
}
|
|
645
|
+
off(event, callback) {
|
|
646
|
+
this.eventEmitter.off(event, callback);
|
|
647
|
+
}
|
|
648
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { EventEmitter } from "./event-emitter.js";
|
|
2
|
+
export { Logger, LogLevel } from "./logger.js";
|
|
3
|
+
export { Button } from "./component/button.js";
|
|
4
|
+
export { Category } from "./component/category.js";
|
|
5
|
+
export { Collapsible } from "./component/collapsible.js";
|
|
6
|
+
export { NumberInput, TextInput } from "./component/input.js";
|
|
7
|
+
export { Selector } from "./component/selector.js";
|
|
8
|
+
export { Slider } from "./component/slider.js";
|
|
9
|
+
export { Switch } from "./component/switch.js";
|
|
10
|
+
export { TextView, TextView as Text } from "./component/text.js";
|
|
11
|
+
export { UIComponent } from "./component/ui-components.js";
|
|
12
|
+
export { ConstantConfig } from "./constant-config.js";
|
|
13
|
+
export { CheckBoxGroup } from "./component/checkBox.js";
|
|
14
|
+
export { FloatMenu, FloatMenuIconOptions, FloatMenuOptions, FloatMenuSetup, FloatMenuSnapshot, MenuPresentation, TabDefinition, } from "./float-menu.js";
|
|
15
|
+
export { LifecycleState } from "./runtime/lifecycle-controller.js";
|
|
16
|
+
export { JavaBridgeCompatibilityError } from "./android-runtime.js";
|
|
17
|
+
export { DEFAULT_PERMISSION_DENIED_MESSAGE, InvalidOverlayIconError, OverlayPermissionError, } from "./runtime/overlay-options.js";
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { EventEmitter } from "./event-emitter.js";
|
|
2
|
+
export { Logger } from "./logger.js";
|
|
3
|
+
export { Button } from "./component/button.js";
|
|
4
|
+
export { Category } from "./component/category.js";
|
|
5
|
+
export { Collapsible } from "./component/collapsible.js";
|
|
6
|
+
export { NumberInput, TextInput } from "./component/input.js";
|
|
7
|
+
export { Selector } from "./component/selector.js";
|
|
8
|
+
export { Slider } from "./component/slider.js";
|
|
9
|
+
export { Switch } from "./component/switch.js";
|
|
10
|
+
export { TextView, TextView as Text } from "./component/text.js";
|
|
11
|
+
export { UIComponent } from "./component/ui-components.js";
|
|
12
|
+
export { ConstantConfig } from "./constant-config.js";
|
|
13
|
+
export { CheckBoxGroup } from "./component/checkBox.js";
|
|
14
|
+
export { FloatMenu, } from "./float-menu.js";
|
|
15
|
+
export { JavaBridgeCompatibilityError } from "./android-runtime.js";
|
|
16
|
+
export { DEFAULT_PERMISSION_DENIED_MESSAGE, InvalidOverlayIconError, OverlayPermissionError, } from "./runtime/overlay-options.js";
|