@everframe/react-native 1.5.0 → 1.6.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/EverframeRN.podspec +1 -1
- package/README.md +136 -17
- package/android/build.gradle.kts +12 -19
- package/android/src/main/java/dev/everframe/rn/EverframeModule.kt +16 -962
- package/dist/NativeEverframe.d.ts +7 -67
- package/dist/index.d.ts +63 -178
- package/dist/index.js +23 -23
- package/dist/index.web.d.ts +2 -2
- package/dist/{launch-C8wbMunK.d.ts → launch-aXtOOfNU.d.ts} +1 -2
- package/ios/Sources/EverframeBridge.swift +16 -456
- package/ios/Sources/EverframeEventEmitter.swift +9 -126
- package/ios/Sources/EverframeModule.mm +2 -52
- package/package.json +4 -4
- package/src/NativeEverframe.ts +11 -107
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
// recordScreen(name, data?) → void (spec 2026-07-14)
|
|
18
18
|
// reportCrash(crashJson) → boolean (SYNC — spec 2026-07-18)
|
|
19
19
|
// setUser(user?) → void (spec 2026-08-12)
|
|
20
|
-
// configure(opts.attachPinUi?) → void (spec 2026-08-19)
|
|
21
|
-
// startCompanion()/stopCompanion() → void (Plan 06.2-11)
|
|
22
|
-
// signalCompanionReportRequestReady(id) → void (Plan 06.2-13)
|
|
23
20
|
// trackPlayer/detachPlayer/recordPlayerEvent/updatePlayerStats/
|
|
24
21
|
// trackVitals → void (spec 2026-09-06)
|
|
25
22
|
// configureSync(opts.vitals*) → boolean (spec 2026-09-06)
|
|
@@ -57,24 +54,10 @@ import com.facebook.react.bridge.ReactMethod
|
|
|
57
54
|
import com.facebook.react.bridge.ReadableMap
|
|
58
55
|
import com.facebook.react.bridge.ReadableType
|
|
59
56
|
import com.facebook.react.bridge.UiThreadUtil
|
|
60
|
-
import com.facebook.react.bridge.WritableMap
|
|
61
57
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
62
58
|
import com.facebook.react.module.annotations.ReactModule
|
|
63
59
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
64
60
|
import dev.everframe.Everframe
|
|
65
|
-
import dev.everframe.capture.ScreenshotCapture
|
|
66
|
-
import dev.everframe.capture.SensitiveRectRegistry
|
|
67
|
-
import dev.everframe.companion.AttachChallengeInfo
|
|
68
|
-
import dev.everframe.companion.AttachPinUi
|
|
69
|
-
import dev.everframe.companion.CompanionBadge
|
|
70
|
-
import dev.everframe.companion.CompanionBadgeOptions
|
|
71
|
-
import dev.everframe.companion.CompanionCaptureBridge
|
|
72
|
-
import dev.everframe.companion.CompanionDeviceFacts
|
|
73
|
-
import dev.everframe.companion.CompanionState
|
|
74
|
-
import dev.everframe.companion.CompanionSubmissionComposer
|
|
75
|
-
import dev.everframe.companion.PreviewCapture
|
|
76
|
-
import dev.everframe.companion.RelayWSClient
|
|
77
|
-
import dev.everframe.companion.parseCompanionBadgePosition
|
|
78
61
|
import dev.everframe.config.ReleaseHealthConfig
|
|
79
62
|
import dev.everframe.config.ReleaseHealthBundleStatus
|
|
80
63
|
import dev.everframe.config.CaptureConfig
|
|
@@ -83,22 +66,10 @@ import dev.everframe.config.ReportResult
|
|
|
83
66
|
import dev.everframe.config.ReporterThemeOptions
|
|
84
67
|
import dev.everframe.config.TXUser
|
|
85
68
|
import dev.everframe.config.EverframeConfig
|
|
86
|
-
import dev.everframe.protocol.generated.ReportAssembled
|
|
87
|
-
import dev.everframe.protocol.generated.ReportAssembledCounts
|
|
88
|
-
import dev.everframe.protocol.generated.ReportAssembledToggles
|
|
89
|
-
import dev.everframe.protocol.generated.ReportSubmit
|
|
90
|
-
import kotlinx.coroutines.CoroutineScope
|
|
91
|
-
import kotlinx.coroutines.Dispatchers
|
|
92
|
-
import kotlinx.coroutines.Job
|
|
93
69
|
import kotlinx.coroutines.MainScope
|
|
94
|
-
import kotlinx.coroutines.SupervisorJob
|
|
95
|
-
import kotlinx.coroutines.cancel
|
|
96
|
-
import kotlinx.coroutines.flow.collect
|
|
97
70
|
import kotlinx.coroutines.launch
|
|
98
71
|
import kotlinx.coroutines.runBlocking
|
|
99
72
|
import kotlinx.coroutines.withContext
|
|
100
|
-
import okhttp3.OkHttpClient
|
|
101
|
-
import java.io.ByteArrayOutputStream
|
|
102
73
|
import java.util.concurrent.ConcurrentHashMap
|
|
103
74
|
|
|
104
75
|
@ReactModule(name = EverframeModule.NAME)
|
|
@@ -167,10 +138,8 @@ class EverframeModule(
|
|
|
167
138
|
|
|
168
139
|
init {
|
|
169
140
|
// Extra-resolver ask-and-wait (spec 2026-09-17 setExtra-resolver).
|
|
170
|
-
// Installed unconditionally, at construction
|
|
171
|
-
//
|
|
172
|
-
// are, because this must also cover the plain in-app openReporter()
|
|
173
|
-
// path and native shake (both funnel through Everframe.report.open(),
|
|
141
|
+
// Installed unconditionally, at construction, because this must cover
|
|
142
|
+
// the plain in-app openReporter() path and native shake (both funnel through Everframe.report.open(),
|
|
174
143
|
// which calls Everframe.consumePendingAttachments() — see that
|
|
175
144
|
// property's doc comment on Everframe.kt). Reassigning on every
|
|
176
145
|
// module construction is intentional and harmless: the newest
|
|
@@ -188,57 +157,12 @@ class EverframeModule(
|
|
|
188
157
|
/** Crash-only sessions (spec 2026-10-12). The JS facade maps this exact code to `EverframeReporterDisabledError`. */
|
|
189
158
|
internal const val REJECT_REPORTER_DISABLED: String = "EVERFRAME_REPORTER_DISABLED"
|
|
190
159
|
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// `EverframeEventEmitter.swift` constants.
|
|
194
|
-
internal const val COMPANION_STATE_EVENT: String = "everframe.companion.state"
|
|
195
|
-
internal const val COMPANION_PAIR_URL_EVENT: String = "everframe.companion.pairUrl"
|
|
196
|
-
|
|
197
|
-
// Companion discovery (spec 2026-08-07). `code` is the short display
|
|
198
|
-
// code from `/api/companion/announce`, rendered beside the QR so a
|
|
199
|
-
// team member reading the TV screen can pick this device out of the
|
|
200
|
-
// dashboard list. `attachedUserName` is the dashboard member who
|
|
201
|
-
// attached, null on an ordinary QR bond. Both cross as String | null.
|
|
202
|
-
internal const val COMPANION_CODE_EVENT: String = "everframe.companion.code"
|
|
203
|
-
internal const val COMPANION_ATTACHED_USER_NAME_EVENT: String =
|
|
204
|
-
"everframe.companion.attachedUserName"
|
|
205
|
-
// Server-resolved device display name (naming spec 2026-08-24) — e.g.
|
|
206
|
-
// an org-configured friendly name for this device. Null before the
|
|
207
|
-
// first successful announce or when discovery never ran. Crosses as
|
|
208
|
-
// String | null.
|
|
209
|
-
internal const val COMPANION_RESOLVED_NAME_EVENT: String =
|
|
210
|
-
"everframe.companion.resolvedName"
|
|
211
|
-
// Pending dashboard-initiated attach-PIN challenge (spec
|
|
212
|
-
// 2026-08-19). `{ code, requestedByName, ttlMs }` or null — see
|
|
213
|
-
// `installCompanionCollectorsIfNeeded()`'s attachChallenge collector
|
|
214
|
-
// below for the full contract.
|
|
215
|
-
internal const val COMPANION_ATTACH_CHALLENGE_EVENT: String =
|
|
216
|
-
"everframe.companion.attachChallenge"
|
|
217
|
-
// Fired when a `report.request` arrives from the paired phone. The
|
|
218
|
-
// event and its `signalCompanionReportRequestReady` acknowledgement
|
|
219
|
-
// are the companion's JS-ready handshake — the bounded wait is part
|
|
220
|
-
// of the pairing flow.
|
|
221
|
-
internal const val COMPANION_REPORT_REQUESTED_EVENT: String = "everframe.companion.reportRequested"
|
|
222
|
-
|
|
223
|
-
// Spec 2026-09-17 setExtra-resolver — NOT a companion event (no
|
|
224
|
-
// "companion." prefix): fired for the plain in-app openReporter()
|
|
225
|
-
// path and native shake too, not just the phone-companion flow. See
|
|
160
|
+
// Spec 2026-09-17 setExtra-resolver: fired for the plain in-app
|
|
161
|
+
// openReporter() path and native shake. See
|
|
226
162
|
// `awaitJsExtraResolve()` below and `packages/sdk-react-native/src/
|
|
227
163
|
// runtime.ts`'s matching listener.
|
|
228
164
|
internal const val EXTRA_RESOLVE_REQUESTED_EVENT: String = "everframe.extra.resolveRequested"
|
|
229
165
|
|
|
230
|
-
/**
|
|
231
|
-
* Stringify a `CompanionState` for the RN event payload. Snake_case
|
|
232
|
-
* mirrors the iOS `CompanionState.rawValue` shape ("unpaired",
|
|
233
|
-
* "paired", "report_in_progress", "phone_disconnected") so JS
|
|
234
|
-
* consumers can treat both platforms with a single union type.
|
|
235
|
-
*/
|
|
236
|
-
internal fun CompanionState.toRnString(): String = when (this) {
|
|
237
|
-
CompanionState.Unpaired -> "unpaired"
|
|
238
|
-
CompanionState.Paired -> "paired"
|
|
239
|
-
CompanionState.ReportInProgress -> "report_in_progress"
|
|
240
|
-
CompanionState.PhoneDisconnected -> "phone_disconnected"
|
|
241
|
-
}
|
|
242
166
|
}
|
|
243
167
|
|
|
244
168
|
// getName() is inherited from NativeEverframeSpec (returns "Everframe").
|
|
@@ -306,16 +230,6 @@ class EverframeModule(
|
|
|
306
230
|
// (`reporterDisabled`, flattened from `reporter: { disabled }`),
|
|
307
231
|
// native stores the permission.
|
|
308
232
|
val reporterDisabled = opts.takeIfHasBoolean("reporterDisabled") ?: false
|
|
309
|
-
// Attach-PIN UI mode (spec 2026-08-19) — a flat optional STRING
|
|
310
|
-
// field (codegen cannot express a string-literal union), parsed
|
|
311
|
-
// here and stashed for `startCompanion()` to read (the flag is a
|
|
312
|
-
// startup-time preference riding `configure()`, not a per-call
|
|
313
|
-
// argument; see NativeEverframe.ts's D-decision header note).
|
|
314
|
-
// Absent/unrecognised both coerce to BUILTIN, the safe default
|
|
315
|
-
// matching "host did nothing special". Parsed into a LOCAL: the
|
|
316
|
-
// idempotence gate below compares it against the stashed value
|
|
317
|
-
// before deciding, and only the start path adopts it.
|
|
318
|
-
val attachPinUi = parseAttachPinUi(opts.takeIfHasString("attachPinUi"))
|
|
319
233
|
|
|
320
234
|
val config = EverframeConfig(
|
|
321
235
|
// The JS config carries no App ID, and the native SDK requires a
|
|
@@ -335,26 +249,9 @@ class EverframeModule(
|
|
|
335
249
|
// The JS facade always sends React Native's `__DEV__` (or the host's override). A missing
|
|
336
250
|
// or null key stays null so the native SDK decides for itself, as for any other caller.
|
|
337
251
|
debugBuild = opts.takeIfHasBoolean("debugBuild"),
|
|
338
|
-
// Explicit companion device-identity override (naming spec
|
|
339
|
-
// 2026-08-24) — read straight off the flat ConfigOpts field,
|
|
340
|
-
// same shape as attachPinUi above. Absent/blank falls
|
|
341
|
-
// through to the SSAID-then-stored-UUID chain inside
|
|
342
|
-
// CompanionDeviceId.resolve.
|
|
343
|
-
companionDeviceId = opts.takeIfHasString("companionDeviceId"),
|
|
344
|
-
// Companion on-screen name badge (naming spec 2026-08-24,
|
|
345
|
-
// controller ruling / Task 6b) — flat fields on ConfigOpts
|
|
346
|
-
// (RN codegen cannot express the nested `{enabled,
|
|
347
|
-
// position}` shape the JS facade's
|
|
348
|
-
// `RuntimeConfig.companionBadge` uses; `extractBridgeConfig`
|
|
349
|
-
// flattens it before it crosses). Absent `enabled` defaults
|
|
350
|
-
// to `true` (badge shown); `position` is stashed raw here
|
|
351
|
-
// and parsed into `CompanionBadgePosition` only at
|
|
352
|
-
// `startCompanion()` time, mirroring `attachPinUi`.
|
|
353
|
-
companionBadgeEnabled = opts.takeIfHasBoolean("companionBadgeEnabled") ?: true,
|
|
354
252
|
shakeToReportEnabled = opts.takeIfHasBoolean("shakeToReportEnabled") ?: true,
|
|
355
253
|
quickCaptureEnabled = opts.takeIfHasBoolean("quickCaptureEnabled") ?: true,
|
|
356
254
|
reporterEnabled = !reporterDisabled,
|
|
357
|
-
companionBadgePosition = opts.takeIfHasString("companionBadgePosition"),
|
|
358
255
|
// Inline reporter theme (reporter branding spec 2026-08-25,
|
|
359
256
|
// RN slice) — 8 flat optional STRING fields on ConfigOpts
|
|
360
257
|
// (RN codegen cannot express the nested `theme: { … }` shape
|
|
@@ -406,16 +303,11 @@ class EverframeModule(
|
|
|
406
303
|
// So compare against what is actually INSTALLED. `EverframeConfig` is a data class,
|
|
407
304
|
// so `==` is structural over every field a start would write, which makes the
|
|
408
305
|
// comparison exhaustive by construction — a new config field can never be forgotten
|
|
409
|
-
// here the way a hand-maintained key list could be. The
|
|
306
|
+
// here the way a hand-maintained key list could be. The extra term:
|
|
410
307
|
// • `captureGate` — a killed SDK must be restarted even by an identical config,
|
|
411
308
|
// or the host is left with a dead SDK and no way to revive it.
|
|
412
|
-
// • `attachPinUi` — the one bridge option that lands in module state instead of
|
|
413
|
-
// in `EverframeConfig`, so config equality alone cannot see it change.
|
|
414
309
|
interactionEventObserver.refresh(reactContext)
|
|
415
|
-
if (Everframe.captureGate &&
|
|
416
|
-
Everframe.currentConfig == config &&
|
|
417
|
-
companionAttachPinUi == attachPinUi
|
|
418
|
-
) {
|
|
310
|
+
if (Everframe.captureGate && Everframe.currentConfig == config) {
|
|
419
311
|
android.util.Log.d(
|
|
420
312
|
"Everframe.rn",
|
|
421
313
|
"configure: SDK already running this exact config — skipping start()",
|
|
@@ -425,11 +317,6 @@ class EverframeModule(
|
|
|
425
317
|
return@txGuardSurface
|
|
426
318
|
}
|
|
427
319
|
|
|
428
|
-
// Stash the attach-PIN UI mode for startCompanion() to read (spec 2026-08-19). It
|
|
429
|
-
// is written only on the start path: the skip path above has already established
|
|
430
|
-
// that the stashed value equals the parsed one.
|
|
431
|
-
companionAttachPinUi = attachPinUi
|
|
432
|
-
|
|
433
320
|
// Pass the live Activity: configure() runs from JS long after
|
|
434
321
|
// MainActivity's onCreate, so without it the tap adapter's
|
|
435
322
|
// Window.Callback wrapper is never installed on the current screen
|
|
@@ -467,7 +354,7 @@ class EverframeModule(
|
|
|
467
354
|
* { status: "cancelled" }
|
|
468
355
|
*
|
|
469
356
|
* Rejects with [REJECT_REPORTER_DISABLED] in a crash-only session
|
|
470
|
-
* (`reporterDisabled`)
|
|
357
|
+
* (`reporterDisabled`).
|
|
471
358
|
*/
|
|
472
359
|
@ReactMethod
|
|
473
360
|
override fun openReporter(promise: Promise) {
|
|
@@ -575,8 +462,7 @@ class EverframeModule(
|
|
|
575
462
|
* because it is written from the JS thread (this `@ReactMethod`) and
|
|
576
463
|
* read from `awaitJsExtraResolve`, which runs wherever
|
|
577
464
|
* `Everframe.consumePendingAttachments()`'s caller happens to run
|
|
578
|
-
* (Main for `openReporter()`/shake
|
|
579
|
-
* companion path).
|
|
465
|
+
* (Main for `openReporter()`/shake).
|
|
580
466
|
*/
|
|
581
467
|
@Volatile
|
|
582
468
|
private var extraResolverActive: Boolean = false
|
|
@@ -857,8 +743,7 @@ class EverframeModule(
|
|
|
857
743
|
* IO scope and is a silent no-op pre-start, so there is nothing a JVM
|
|
858
744
|
* unit test could otherwise assert on). Production default forwards to
|
|
859
745
|
* the core singleton; `RnReplayBridgeTest` swaps it to a recording
|
|
860
|
-
* lambda and restores it.
|
|
861
|
-
* `hasCompanionClientForTesting()` above.
|
|
746
|
+
* lambda and restores it.
|
|
862
747
|
*/
|
|
863
748
|
internal var __drainRequester: () -> Unit = { Everframe.requestOutboxDrain() }
|
|
864
749
|
|
|
@@ -889,717 +774,12 @@ class EverframeModule(
|
|
|
889
774
|
sensitiveRegistration.register(viewTag)
|
|
890
775
|
}
|
|
891
776
|
|
|
892
|
-
// ---------------- Companion (Plan 06.2-11) ----------------
|
|
893
|
-
//
|
|
894
|
-
// The bridge owns at most ONE live `RelayWSClient` per process.
|
|
895
|
-
// `startCompanion()` is idempotent: a second call while a client is
|
|
896
|
-
// already live is a no-op — the socket is NOT recreated. Call
|
|
897
|
-
// `stopCompanion()` first to tear the connection down before re-starting.
|
|
898
|
-
//
|
|
899
|
-
// External review, finding NN1 — "at most one per process" is now
|
|
900
|
-
// enforced ACROSS this bridge AND `Everframe.startCompanion()` (a plain-
|
|
901
|
-
// native host's own companion entry point), not just within this
|
|
902
|
-
// module: both route through `Everframe.__registerCompanionClient` /
|
|
903
|
-
// `__unregisterCompanionClient`, a single `@Synchronized` slot on the
|
|
904
|
-
// core facade. First caller wins; this bridge's `startCompanion()`
|
|
905
|
-
// discards its own just-built client (never starting it or installing
|
|
906
|
-
// any provider seam for it) when a native host already holds the slot,
|
|
907
|
-
// and its `stopCompanion()` only ever stops/unregisters the client THIS
|
|
908
|
-
// module itself registered.
|
|
909
|
-
//
|
|
910
|
-
// StateFlow collection lives on a module-owned `CoroutineScope` (Main +
|
|
911
|
-
// SupervisorJob, mirroring the precedent in CompanionCaptureBridge.kt's
|
|
912
|
-
// service scope shape). Collectors stay alive across stop/start cycles
|
|
913
|
-
// because the underlying `Everframe.companion` StateFlows survive the
|
|
914
|
-
// WS lifetime — re-installing on every start would race the initial
|
|
915
|
-
// `__setState(Unpaired)` emission on cold start and lose it.
|
|
916
|
-
//
|
|
917
|
-
// The events forwarded to JS mirror iOS exactly:
|
|
918
|
-
// • `everframe.companion.state` — snake_case CompanionState
|
|
919
|
-
// • `everframe.companion.pairUrl` — String | null
|
|
920
|
-
// • `everframe.companion.code` — String | null
|
|
921
|
-
// • `everframe.companion.attachedUserName` — String | null
|
|
922
|
-
// • `everframe.companion.attachChallenge` — WritableMap{code,requestedByName,ttlMs} | null (spec 2026-08-19)
|
|
923
|
-
//
|
|
924
|
-
// The collector Jobs below are never cancelled: `companionScope` and the
|
|
925
|
-
// module live for the process, and the collectors deliberately outlive
|
|
926
|
-
// stop/start cycles (see above). The fields exist so the jobs are
|
|
927
|
-
// inspectable and so a future teardown path has something to cancel.
|
|
928
|
-
//
|
|
929
|
-
// Emission is via `RCTDeviceEventEmitter` (the default emitter); JS
|
|
930
|
-
// facade uses `new NativeEventEmitter()` on Android (no module ref
|
|
931
|
-
// needed — the default emitter is platform-supplied).
|
|
932
|
-
|
|
933
|
-
private val companionScope: CoroutineScope =
|
|
934
|
-
CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
935
|
-
|
|
936
|
-
@Volatile
|
|
937
|
-
private var companionClient: RelayWSClient? = null
|
|
938
|
-
|
|
939
|
-
@Volatile
|
|
940
|
-
private var companionCollectorsInstalled: Boolean = false
|
|
941
|
-
|
|
942
|
-
@Volatile
|
|
943
|
-
private var companionStateJob: Job? = null
|
|
944
|
-
|
|
945
|
-
@Volatile
|
|
946
|
-
private var companionPairUrlJob: Job? = null
|
|
947
|
-
|
|
948
|
-
@Volatile
|
|
949
|
-
private var companionCodeJob: Job? = null
|
|
950
|
-
|
|
951
|
-
@Volatile
|
|
952
|
-
private var companionAttachedUserNameJob: Job? = null
|
|
953
|
-
|
|
954
|
-
@Volatile
|
|
955
|
-
private var companionResolvedNameJob: Job? = null
|
|
956
|
-
|
|
957
|
-
@Volatile
|
|
958
|
-
private var companionAttachChallengeJob: Job? = null
|
|
959
|
-
|
|
960
|
-
/**
|
|
961
|
-
* The host's configured attach-PIN UI mode (spec 2026-08-19), parsed
|
|
962
|
-
* from `ConfigOpts.attachPinUi` at `configure()` time and read by
|
|
963
|
-
* `startCompanion()` — both to pass to `RelayWSClient` (the announce
|
|
964
|
-
* capability) and to set `Everframe.__attachPinUiSuppressed` (the
|
|
965
|
-
* built-in presenter's runtime silence flag). Defaults to `BUILTIN` —
|
|
966
|
-
* pre-existing behaviour for a host that never set the flag.
|
|
967
|
-
*/
|
|
968
|
-
@Volatile
|
|
969
|
-
private var companionAttachPinUi: AttachPinUi = AttachPinUi.BUILTIN
|
|
970
|
-
|
|
971
|
-
/** Coerce the flat `ConfigOpts.attachPinUi` string into [AttachPinUi].
|
|
972
|
-
* `null` (field absent) and any unrecognised value both degrade to
|
|
973
|
-
* `BUILTIN`. */
|
|
974
|
-
private fun parseAttachPinUi(raw: String?): AttachPinUi = when (raw) {
|
|
975
|
-
"custom" -> AttachPinUi.CUSTOM
|
|
976
|
-
"off" -> AttachPinUi.OFF
|
|
977
|
-
else -> AttachPinUi.BUILTIN
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
// `parseCompanionBadgePosition` moved to `dev.everframe.companion`
|
|
981
|
-
// (CompanionBadge.kt) — external review, finding N3 — so the core
|
|
982
|
-
// facade's own native-host companion start path (`Everframe.startCompanion`)
|
|
983
|
-
// can share it instead of drifting a second copy. Imported above.
|
|
984
|
-
|
|
985
|
-
/** WritableMap has no `long` accessor — `ttlMs` crosses as a double, per
|
|
986
|
-
* the spec's documented `everframe.companion.attachChallenge` payload
|
|
987
|
-
* contract. Extracted so the collector body above stays as short as
|
|
988
|
-
* its siblings. */
|
|
989
|
-
private fun AttachChallengeInfo.toWritableMap(): WritableMap =
|
|
990
|
-
Arguments.createMap().apply {
|
|
991
|
-
putString("code", code)
|
|
992
|
-
putString("requestedByName", requestedByName)
|
|
993
|
-
putDouble("ttlMs", ttlMs.toDouble())
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
/** Test seam — JVM unit test (`EverframeCompanionModuleTest.kt`) asserts
|
|
997
|
-
* the bridge installs a client on startCompanion() and clears it on
|
|
998
|
-
* stopCompanion(). Intentionally `internal` so it doesn't leak past
|
|
999
|
-
* the module boundary. */
|
|
1000
|
-
internal fun hasCompanionClientForTesting(): Boolean = companionClient != null
|
|
1001
|
-
|
|
1002
|
-
/** Test seam — the live client instance (or null), so tests can assert by
|
|
1003
|
-
* identity that a repeat `startCompanion()` did NOT recreate the socket. */
|
|
1004
|
-
internal fun companionClientForTesting(): RelayWSClient? = companionClient
|
|
1005
|
-
|
|
1006
|
-
/**
|
|
1007
|
-
* The SDK key the host already configured through `configure()`, or null
|
|
1008
|
-
* when there isn't a usable one. Read straight off `Everframe`'s own config
|
|
1009
|
-
* snapshot — deliberately NOT a second place a key can be configured, and
|
|
1010
|
-
* NOT a new public surface (`currentConfig` is already public read-only).
|
|
1011
|
-
*
|
|
1012
|
-
* Null when `Everframe.start` never ran, and when the host configured a
|
|
1013
|
-
* blank key. Both are "no dashboard presence", never "no socket".
|
|
1014
|
-
*/
|
|
1015
|
-
private fun configuredSdkKey(): String? =
|
|
1016
|
-
Everframe.currentConfig?.sdkKey?.takeIf { it.isNotBlank() }
|
|
1017
|
-
|
|
1018
|
-
/**
|
|
1019
|
-
* Non-personal device label for the dashboard's device list — the platform
|
|
1020
|
-
* model ("Chromecast", "AFTKA", "Pixel 8"). Deliberately
|
|
1021
|
-
* `android.os.Build.MODEL` and not any user-assigned device name, which is
|
|
1022
|
-
* commonly a person's name and would leak into a shared dashboard list as
|
|
1023
|
-
* a side effect of turning companion on. The display code is the real
|
|
1024
|
-
* disambiguator; the label is only a convenience.
|
|
1025
|
-
*
|
|
1026
|
-
* `Build.MODEL` is a platform type — it reads back null under plain JVM
|
|
1027
|
-
* unit tests (unmocked android.jar), so the null-safe call is load-bearing
|
|
1028
|
-
* and not defensive noise.
|
|
1029
|
-
*/
|
|
1030
|
-
private fun companionDeviceLabel(): String? =
|
|
1031
|
-
android.os.Build.MODEL?.takeIf { it.isNotBlank() }
|
|
1032
|
-
|
|
1033
|
-
@ReactMethod
|
|
1034
|
-
override fun startCompanion() {
|
|
1035
|
-
if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) {
|
|
1036
|
-
android.os.Handler(android.os.Looper.getMainLooper()).post { startCompanion() }
|
|
1037
|
-
return
|
|
1038
|
-
}
|
|
1039
|
-
txGuardVoid("startCompanion") {
|
|
1040
|
-
// Idempotent: a live client means a prior startCompanion already
|
|
1041
|
-
// opened the socket. Do NOT recreate it — callers must
|
|
1042
|
-
// stopCompanion() first. This keeps repeat starts from spinning up
|
|
1043
|
-
// a second WS (which would race on `pair.created` / `__setPairUrl`).
|
|
1044
|
-
if (companionClient != null) {
|
|
1045
|
-
android.util.Log.i(
|
|
1046
|
-
"Everframe.rn",
|
|
1047
|
-
"startCompanion ignored — already running; call stopCompanion() first",
|
|
1048
|
-
)
|
|
1049
|
-
return@txGuardVoid
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// Await the main-thread install before the relay can announce PIN capability.
|
|
1053
|
-
// configureSync itself remains non-blocking with respect to main.
|
|
1054
|
-
reporterBridge.scheduleInstall(reactContext)
|
|
1055
|
-
installCompanionCollectorsIfNeeded()
|
|
1056
|
-
|
|
1057
|
-
// Runtime suppression of the built-in PIN presenter (spec
|
|
1058
|
-
// 2026-08-19, controller ruling): a runtime flag, not an
|
|
1059
|
-
// install-site gate. `:everframe-reporter-ui`'s
|
|
1060
|
-
// `CompanionPinPresenter` is auto-installed via
|
|
1061
|
-
// `ReporterResolverInstaller` (Startup or the bridge above) regardless of
|
|
1062
|
-
// the configured mode, so `Everframe.__attachPinUiInstalled`
|
|
1063
|
-
// stays an honest capability signal — only the PRESENTING is
|
|
1064
|
-
// silenced, here, at the one site where the mode is already
|
|
1065
|
-
// known (configure() precedes startCompanion() in every host's
|
|
1066
|
-
// lifecycle).
|
|
1067
|
-
Everframe.__attachPinUiSuppressed = companionAttachPinUi != AttachPinUi.BUILTIN
|
|
1068
|
-
|
|
1069
|
-
// RelayWSClient defaults baseUrl to IngestEndpoint.url —
|
|
1070
|
-
// release AAR = https://everframe.dev, debug AAR =
|
|
1071
|
-
// EVERFRAME_DEV_INGEST_URL env var or 10.0.2.2:8787 fallback.
|
|
1072
|
-
//
|
|
1073
|
-
// Companion discovery (spec 2026-08-07): announcing is what makes
|
|
1074
|
-
// this device appear in the dashboard's Companion tab. It rides
|
|
1075
|
-
// the SDK key the host ALREADY configured via `configure()` — we
|
|
1076
|
-
// never ask for a second one. A null key takes the ticketless
|
|
1077
|
-
// path: RelayWSClient builds no announcer and connects to plain
|
|
1078
|
-
// `/relay/tv`, byte-identical to pre-companion behaviour.
|
|
1079
|
-
// Announce must never be able to break reporting — no key means
|
|
1080
|
-
// "no dashboard presence", never "no socket".
|
|
1081
|
-
val client = RelayWSClient(
|
|
1082
|
-
client = OkHttpClient(),
|
|
1083
|
-
sdkKey = configuredSdkKey(),
|
|
1084
|
-
deviceLabel = companionDeviceLabel(),
|
|
1085
|
-
// Naming spec 2026-08-24 — resolved fresh on every announce
|
|
1086
|
-
// attempt (see `RelayWSClient.deviceProvider`'s own doc for
|
|
1087
|
-
// why no caching is needed here). `companionDeviceId` is read
|
|
1088
|
-
// off the same `Everframe.currentConfig` snapshot
|
|
1089
|
-
// `configuredSdkKey()` above already reads, not a second
|
|
1090
|
-
// configuration surface.
|
|
1091
|
-
deviceProvider = {
|
|
1092
|
-
CompanionDeviceFacts.current(
|
|
1093
|
-
reactContext.applicationContext,
|
|
1094
|
-
Everframe.currentConfig?.companionDeviceId,
|
|
1095
|
-
)
|
|
1096
|
-
},
|
|
1097
|
-
attachPinUi = companionAttachPinUi,
|
|
1098
|
-
// Companion on-screen name badge (naming spec 2026-08-24,
|
|
1099
|
-
// controller ruling / Task 6b) — same `Everframe.currentConfig`
|
|
1100
|
-
// snapshot `deviceProvider` above already reads, read fresh
|
|
1101
|
-
// at start time (a toggle/position can't affect anything
|
|
1102
|
-
// before a relay client and its badge exist). `?: true`
|
|
1103
|
-
// covers the same "never configured" edge `configuredSdkKey()`'s
|
|
1104
|
-
// own null-safe chain already tolerates.
|
|
1105
|
-
companionBadge = CompanionBadgeOptions(
|
|
1106
|
-
enabled = Everframe.currentConfig?.companionBadgeEnabled ?: true,
|
|
1107
|
-
position = parseCompanionBadgePosition(Everframe.currentConfig?.companionBadgePosition),
|
|
1108
|
-
),
|
|
1109
|
-
)
|
|
1110
|
-
|
|
1111
|
-
// External review, finding NN1 — register through the core
|
|
1112
|
-
// facade's SINGLE ownership slot instead of just assigning
|
|
1113
|
-
// `companionClient` directly. First caller to register wins:
|
|
1114
|
-
// a native host that already called `Everframe.startCompanion()`
|
|
1115
|
-
// in this same process (or a concurrent RN start that beat us to
|
|
1116
|
-
// it) keeps its client live, and OUR freshly-built one never
|
|
1117
|
-
// goes anywhere near a provider seam or `client.start()` — a
|
|
1118
|
-
// stale/duplicate RN provider must never be able to clobber
|
|
1119
|
-
// whatever the winning caller installed. See
|
|
1120
|
-
// `Everframe.__registerCompanionClient`'s own doc comment.
|
|
1121
|
-
if (!Everframe.__registerCompanionClient(client)) {
|
|
1122
|
-
// Log BEFORE the teardown attempt, not after: this warning is
|
|
1123
|
-
// the one observable signal a host gets that its RN start was
|
|
1124
|
-
// silently discarded, and it must fire even if `client.stop()`
|
|
1125
|
-
// itself throws (DEFE-02 — a never-started client's teardown
|
|
1126
|
-
// is best-effort cleanup, not load-bearing for correctness,
|
|
1127
|
-
// since it was never registered/started in the first place).
|
|
1128
|
-
android.util.Log.w(
|
|
1129
|
-
"Everframe.rn",
|
|
1130
|
-
"companion already started by the native facade — RN start ignored",
|
|
1131
|
-
)
|
|
1132
|
-
runCatching { client.stop() }
|
|
1133
|
-
return@txGuardVoid
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
companionClient = client
|
|
1137
|
-
|
|
1138
|
-
// Plan 06.2-13 Task 3 — drop any prior bridge providers so a
|
|
1139
|
-
// stale closure from a previous startCompanion doesn't double-
|
|
1140
|
-
// handle frames against a now-defunct activity. Safe HERE, only
|
|
1141
|
-
// after winning the registration race above — until then these
|
|
1142
|
-
// providers may belong to someone else entirely.
|
|
1143
|
-
CompanionCaptureBridge.__captureProvider = null
|
|
1144
|
-
CompanionCaptureBridge.__submitProvider = null
|
|
1145
|
-
// Task 11b — same reasoning for the preview-loop and shot-stash
|
|
1146
|
-
// seams: null them before installing fresh ones so a stray call
|
|
1147
|
-
// between stop and start can never run against a stale Activity.
|
|
1148
|
-
CompanionCaptureBridge.__previewProvider = null
|
|
1149
|
-
CompanionCaptureBridge.__shotCaptureProvider = null
|
|
1150
|
-
companionCaptureStash.values.forEach { it.reportCapture.cancel() }
|
|
1151
|
-
companionCaptureStash.clear()
|
|
1152
|
-
// Task 6 (naming spec 2026-08-24) — same reasoning for the name
|
|
1153
|
-
// badge's activity seam: null before installing fresh, exactly
|
|
1154
|
-
// like the removed sharing indicator's own __activityProvider
|
|
1155
|
-
// used to be handled here.
|
|
1156
|
-
CompanionBadge.__activityProvider = null
|
|
1157
|
-
|
|
1158
|
-
installCompanionBridgeProviders()
|
|
1159
|
-
|
|
1160
|
-
client.start()
|
|
1161
|
-
android.util.Log.i("Everframe.rn", "startCompanion: connected")
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
@ReactMethod
|
|
1166
|
-
override fun stopCompanion() {
|
|
1167
|
-
if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) {
|
|
1168
|
-
android.os.Handler(android.os.Looper.getMainLooper()).post { stopCompanion() }
|
|
1169
|
-
return
|
|
1170
|
-
}
|
|
1171
|
-
txGuardVoid("stopCompanion") {
|
|
1172
|
-
// External review, finding NN1 — only ever stop/unregister OUR
|
|
1173
|
-
// OWN client. `companionClient` is non-null here ONLY when a
|
|
1174
|
-
// prior `startCompanion()` on THIS module actually won the
|
|
1175
|
-
// registration race above, so reaching this point already means
|
|
1176
|
-
// we own the providers cleared below — a native host's own
|
|
1177
|
-
// `Everframe.startCompanion()`-registered client (or one still
|
|
1178
|
-
// live from a losing/no-op RN start) is untouched:
|
|
1179
|
-
// `__unregisterCompanionClient`'s identity check would no-op
|
|
1180
|
-
// against it anyway even if `companionClient` somehow pointed
|
|
1181
|
-
// at a stale reference, but the early return means we never even
|
|
1182
|
-
// ask.
|
|
1183
|
-
val client = companionClient ?: return@txGuardVoid
|
|
1184
|
-
client.stop()
|
|
1185
|
-
Everframe.__unregisterCompanionClient(client)
|
|
1186
|
-
companionClient = null
|
|
1187
|
-
// Plan 06.2-13 Task 3 — uninstall bridge providers so the
|
|
1188
|
-
// singleton can't keep firing against a stale activity.
|
|
1189
|
-
CompanionCaptureBridge.__captureProvider = null
|
|
1190
|
-
CompanionCaptureBridge.__submitProvider = null
|
|
1191
|
-
// Task 11b — a preview/shot provider that outlives its session
|
|
1192
|
-
// keeps a stale Activity alive and can capture a screen the
|
|
1193
|
-
// session no longer owns; null both alongside the two above.
|
|
1194
|
-
CompanionCaptureBridge.__previewProvider = null
|
|
1195
|
-
CompanionCaptureBridge.__shotCaptureProvider = null
|
|
1196
|
-
companionCaptureStash.values.forEach { it.reportCapture.cancel() }
|
|
1197
|
-
companionCaptureStash.clear()
|
|
1198
|
-
// Task 6 — a badge activity provider that outlives its session
|
|
1199
|
-
// keeps a stale Activity alive for no reason; RelayWSClient.stop()
|
|
1200
|
-
// (called just above) already tears down the badge's own
|
|
1201
|
-
// subscription/view, this just drops the seam it would have used.
|
|
1202
|
-
CompanionBadge.__activityProvider = null
|
|
1203
|
-
// unclaimed per-report attribution snapshot goes with it. Any
|
|
1204
|
-
// submit already in flight took its token synchronously before
|
|
1205
|
-
// its coroutine was dispatched, so this cannot strip attribution
|
|
1206
|
-
// from a report that is mid-upload.
|
|
1207
|
-
CompanionCaptureBridge.__clearPendingAttribution()
|
|
1208
|
-
// Note: in-flight submit coroutines launched by the bridge's
|
|
1209
|
-
// own submitScope are NOT cancelled here — they're owned by
|
|
1210
|
-
// the bridge object and survive a stop. The next start will
|
|
1211
|
-
// install fresh providers; any straggling submit completes
|
|
1212
|
-
// (or fails) against the now-disconnected WebSocket and the
|
|
1213
|
-
// ws.send(report.completed/failed) becomes a silent drop.
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
// ---------------- Companion provider install (Plan 06.2-13) ----------------
|
|
1218
|
-
|
|
1219
|
-
/**
|
|
1220
|
-
* Plan 06.2-13 Task 3+4 — per-correlation_id stash for capture state
|
|
1221
|
-
* + drained host attachments. Held on the module (NOT on the bridge
|
|
1222
|
-
* singleton) so stopCompanion() can clear it without bridge-internal
|
|
1223
|
-
* state coupling. Mirrors the advisor's note: "ownership flipped (RN
|
|
1224
|
-
* module owns the bridge install lifecycle)".
|
|
1225
|
-
*
|
|
1226
|
-
* `ConcurrentHashMap` because the OkHttp WS thread (write at
|
|
1227
|
-
* report.request) can race with the bridge's submitScope (read at
|
|
1228
|
-
* report.submit).
|
|
1229
|
-
*/
|
|
1230
|
-
private data class CompanionCapture(
|
|
1231
|
-
val capture: ScreenshotCapture.CaptureResult,
|
|
1232
|
-
val reportCapture: dev.everframe.capture.video.FrozenReportCapture,
|
|
1233
|
-
val hostExtra: String?,
|
|
1234
|
-
)
|
|
1235
|
-
|
|
1236
|
-
private val companionCaptureStash = ConcurrentHashMap<String, CompanionCapture>()
|
|
1237
|
-
|
|
1238
|
-
/**
|
|
1239
|
-
* Install both `__captureProvider` and `__submitProvider` on the
|
|
1240
|
-
* `CompanionCaptureBridge` singleton. Closes over `reactContext` for
|
|
1241
|
-
* activity acquisition (RN tracks the foreground activity via
|
|
1242
|
-
* `getCurrentActivity()` — same surface `openReporter` uses).
|
|
1243
|
-
*/
|
|
1244
|
-
private fun installCompanionBridgeProviders() {
|
|
1245
|
-
// ----- __captureProvider -----
|
|
1246
|
-
CompanionCaptureBridge.__captureProvider = { correlationId, reportCapture ->
|
|
1247
|
-
val activity = reactContext.currentActivity
|
|
1248
|
-
if (activity == null) {
|
|
1249
|
-
android.util.Log.w(
|
|
1250
|
-
"Everframe.rn",
|
|
1251
|
-
"companion captureProvider: no foreground activity; emitting empty assembled",
|
|
1252
|
-
)
|
|
1253
|
-
emptyAssembledPayload(correlationId)
|
|
1254
|
-
} else {
|
|
1255
|
-
runCatching {
|
|
1256
|
-
// 0. Ask JS to walk the React fiber tree + attach it
|
|
1257
|
-
// BEFORE we capture native state, then drain it. JS
|
|
1258
|
-
// can no longer attach one (the TurboModule spec
|
|
1259
|
-
// method is gone, spec 2026-08-29); the wait survives
|
|
1260
|
-
// as the companion's JS-ready handshake. Bounded — we
|
|
1261
|
-
// don't block capture forever if JS is non-responsive
|
|
1262
|
-
// (DEFE-02).
|
|
1263
|
-
awaitJsReactTreeAttach(correlationId)
|
|
1264
|
-
// 1. Sensitive-rect walk before capture (mirrors
|
|
1265
|
-
// TXReporterPresenter.kt:51-54).
|
|
1266
|
-
val rects = SensitiveRectRegistry.collectInWindowCoords(activity)
|
|
1267
|
-
// 2. Capture screenshot — suspend, so block on the
|
|
1268
|
-
// OkHttp thread. PixelCopy callback is async; the
|
|
1269
|
-
// runBlocking is bounded by the PixelCopy timeout
|
|
1270
|
-
// inside ScreenshotCapture. Same pattern the
|
|
1271
|
-
// in-process modal uses (TXReporterPresenter
|
|
1272
|
-
// awaits the suspend directly).
|
|
1273
|
-
val capture = runBlocking {
|
|
1274
|
-
ScreenshotCapture.captureBeforeReporter(activity, rects)
|
|
1275
|
-
}
|
|
1276
|
-
if (capture == null) {
|
|
1277
|
-
emptyAssembledPayload(correlationId)
|
|
1278
|
-
} else {
|
|
1279
|
-
// 3. Drain host-attached payload — consume-once
|
|
1280
|
-
// semantics, matches the iOS Plan 06.2-13
|
|
1281
|
-
// Task 1 timing (request time, NOT submit
|
|
1282
|
-
// time). A second back-to-back report sees
|
|
1283
|
-
// nil here. `consumePendingAttachments` is
|
|
1284
|
-
// `suspend` (spec 2026-09-17 setExtra-resolver —
|
|
1285
|
-
// it asks JS for a fresh resolver value before
|
|
1286
|
-
// draining); `runBlocking` here is safe for the
|
|
1287
|
-
// same reason the screenshot capture above is —
|
|
1288
|
-
// this whole provider runs on the OkHttp WS
|
|
1289
|
-
// thread, never the UI thread.
|
|
1290
|
-
val hostExtraRaw =
|
|
1291
|
-
runBlocking { Everframe.consumePendingAttachments() }
|
|
1292
|
-
val hostExtra = hostExtraRaw?.takeIf { it.isNotEmpty() }
|
|
1293
|
-
|
|
1294
|
-
// Serialize the short stash publication with owner teardown.
|
|
1295
|
-
val published = CompanionCaptureBridge.__publishCapture(correlationId, reportCapture) {
|
|
1296
|
-
companionCaptureStash.values.forEach { it.reportCapture.cancel() }
|
|
1297
|
-
companionCaptureStash.clear()
|
|
1298
|
-
companionCaptureStash[correlationId] = CompanionCapture(
|
|
1299
|
-
capture = capture,
|
|
1300
|
-
reportCapture = reportCapture,
|
|
1301
|
-
hostExtra = hostExtra,
|
|
1302
|
-
)
|
|
1303
|
-
}
|
|
1304
|
-
if (!published) {
|
|
1305
|
-
reportCapture.cancel()
|
|
1306
|
-
return@runCatching null
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
// 5. Re-encode the screenshot for the relay hop.
|
|
1310
|
-
// Bitmap.CompressFormat.WEBP_LOSSY at q=85 cuts
|
|
1311
|
-
// typical 1080p screenshot bytes by ~70-80% vs
|
|
1312
|
-
// PNG (API 30+); WEBP (legacy, lossy when
|
|
1313
|
-
// quality<100) covers API 24-29; JPEG @ q=85 is
|
|
1314
|
-
// the universal fallback. The phone consumes
|
|
1315
|
-
// via the announced mime — no branching on the
|
|
1316
|
-
// receiver side.
|
|
1317
|
-
val (relayBytes, relayMime) = reencodeScreenshot(capture.bitmap, capture.pngBytes)
|
|
1318
|
-
|
|
1319
|
-
// 6. Build the AssembledPayload (text frame +
|
|
1320
|
-
// binary screenshot) for the bridge to ship.
|
|
1321
|
-
//
|
|
1322
|
-
// NO second binary frame and NO `tree` metadata:
|
|
1323
|
-
// tap-to-identify was removed (spec 2026-08-29),
|
|
1324
|
-
// so nothing walks, gzips or announces a UI tree.
|
|
1325
|
-
// `toggles.uiTree` / `counts.uiTreeNodes` stay on
|
|
1326
|
-
// the wire because the generated relay types
|
|
1327
|
-
// declare them non-optional, but they are now
|
|
1328
|
-
// always false / 0. Announcing a tree the phone
|
|
1329
|
-
// would then wait for is the failure mode this
|
|
1330
|
-
// guards against — the relay protocol documents
|
|
1331
|
-
// the absent case as supported.
|
|
1332
|
-
val toggles = ReportAssembledToggles(
|
|
1333
|
-
logs = true,
|
|
1334
|
-
metadata = true,
|
|
1335
|
-
network = true,
|
|
1336
|
-
screenshot = true,
|
|
1337
|
-
uiTree = false,
|
|
1338
|
-
)
|
|
1339
|
-
val counts = ReportAssembledCounts(
|
|
1340
|
-
breadcrumbs = dev.everframe.capture.sharedBreadcrumbBuffer.size.toLong(),
|
|
1341
|
-
logs = dev.everframe.capture.sharedLogBuffer.size().toLong(),
|
|
1342
|
-
network = dev.everframe.capture.sharedNetworkBuffer.size().toLong(),
|
|
1343
|
-
uiTreeNodes = 0L,
|
|
1344
|
-
)
|
|
1345
|
-
val assembled = ReportAssembled(
|
|
1346
|
-
correlationId = correlationId,
|
|
1347
|
-
counts = counts,
|
|
1348
|
-
mime = relayMime,
|
|
1349
|
-
size = relayBytes.size.toLong(),
|
|
1350
|
-
toggles = toggles,
|
|
1351
|
-
tree = null,
|
|
1352
|
-
)
|
|
1353
|
-
CompanionCaptureBridge.AssembledPayload(
|
|
1354
|
-
assembled = assembled,
|
|
1355
|
-
pngBytes = relayBytes,
|
|
1356
|
-
)
|
|
1357
|
-
}
|
|
1358
|
-
}.getOrElse { t ->
|
|
1359
|
-
android.util.Log.w(
|
|
1360
|
-
"Everframe.rn",
|
|
1361
|
-
"companion captureProvider threw: ${t.javaClass.simpleName}: ${t.message}",
|
|
1362
|
-
)
|
|
1363
|
-
emptyAssembledPayload(correlationId)
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
// ----- __submitProvider -----
|
|
1369
|
-
// Suspend lambda — the bridge's own submitScope handles dispatch.
|
|
1370
|
-
// `companionAttribution` is the token the relay minted for THIS
|
|
1371
|
-
// report's `report.request`, snapshotted by RelayWSClient at that
|
|
1372
|
-
// moment and handed back here by the bridge (PR-fix 1). It is passed
|
|
1373
|
-
// straight into Inputs and never re-derived from the live session:
|
|
1374
|
-
// this closure survives stopCompanion() and can still be composing
|
|
1375
|
-
// when a different dashboard user attaches.
|
|
1376
|
-
// SECURITY: never log it.
|
|
1377
|
-
CompanionCaptureBridge.__submitProvider = submit@{
|
|
1378
|
-
submit: ReportSubmit,
|
|
1379
|
-
bakedPng: ByteArray,
|
|
1380
|
-
extraShotPngs: List<ByteArray>,
|
|
1381
|
-
companionAttribution: String?,
|
|
1382
|
-
reportCapture: dev.everframe.capture.video.FrozenReportCapture,
|
|
1383
|
-
capturedSession: dev.everframe.TXCapturedSession
|
|
1384
|
-
->
|
|
1385
|
-
val activity = reactContext.currentActivity
|
|
1386
|
-
if (activity == null) {
|
|
1387
|
-
reportCapture.cancel()
|
|
1388
|
-
return@submit suspend { CompanionCaptureBridge.SubmitResult.Err("no_activity") }
|
|
1389
|
-
}
|
|
1390
|
-
val stash = companionCaptureStash[submit.correlationId]
|
|
1391
|
-
?: run { reportCapture.cancel(); return@submit suspend { CompanionCaptureBridge.SubmitResult.Err("no_capture") } }
|
|
1392
|
-
|
|
1393
|
-
if (stash.reportCapture !== reportCapture) {
|
|
1394
|
-
reportCapture.cancel()
|
|
1395
|
-
return@submit suspend { CompanionCaptureBridge.SubmitResult.Err("capture_mismatch") }
|
|
1396
|
-
}
|
|
1397
|
-
if (!companionCaptureStash.remove(submit.correlationId, stash)) {
|
|
1398
|
-
reportCapture.cancel()
|
|
1399
|
-
return@submit suspend { CompanionCaptureBridge.SubmitResult.Err("no_capture") }
|
|
1400
|
-
}
|
|
1401
|
-
// Everything below may be expensive; captured references already belong to this submit.
|
|
1402
|
-
suspend work@{
|
|
1403
|
-
// Decode baked PNG → Bitmap. If the phone sent corrupt bytes
|
|
1404
|
-
// we fall back to the original capture bitmap so the user's
|
|
1405
|
-
// annotations are lost but the report still ships (DEFE-02).
|
|
1406
|
-
val bakedBitmap: android.graphics.Bitmap = try {
|
|
1407
|
-
android.graphics.BitmapFactory.decodeByteArray(
|
|
1408
|
-
bakedPng, 0, bakedPng.size,
|
|
1409
|
-
) ?: stash.capture.bitmap
|
|
1410
|
-
} catch (t: Throwable) {
|
|
1411
|
-
stash.capture.bitmap
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Tap-to-identify was removed (spec 2026-08-29). A phone running
|
|
1415
|
-
// an older reporter SPA may still send `submit.reportTarget`; it
|
|
1416
|
-
// is ignored rather than rejected, and nothing is resolved from it.
|
|
1417
|
-
|
|
1418
|
-
val inputs = CompanionSubmissionComposer.Inputs(
|
|
1419
|
-
activity = activity,
|
|
1420
|
-
captureBitmap = bakedBitmap,
|
|
1421
|
-
capture = reportCapture,
|
|
1422
|
-
// The phone's extra shots, already baked on its side and
|
|
1423
|
-
// delivered behind `shot.binary` markers. Passed through
|
|
1424
|
-
// verbatim: the composer names them `screenshot-N`.
|
|
1425
|
-
extraShotPngs = extraShotPngs,
|
|
1426
|
-
title = submit.title,
|
|
1427
|
-
description = submit.description.text,
|
|
1428
|
-
// Description-redactions integration deferred (matches iOS
|
|
1429
|
-
// Plan 12 — reporter SPA sends redactions:[] always).
|
|
1430
|
-
includeLogs = submit.includes.logs,
|
|
1431
|
-
includeNetwork = submit.includes.network,
|
|
1432
|
-
includeMetadata = submit.includes.metadata,
|
|
1433
|
-
hostExtra = stash.hostExtra,
|
|
1434
|
-
companionAttribution = companionAttribution,
|
|
1435
|
-
// Pinned to the snapshot taken at the submit boundary above.
|
|
1436
|
-
capturedSession = capturedSession,
|
|
1437
|
-
)
|
|
1438
|
-
|
|
1439
|
-
val result = try {
|
|
1440
|
-
CompanionSubmissionComposer.submit(inputs)
|
|
1441
|
-
} catch (t: Throwable) {
|
|
1442
|
-
android.util.Log.w(
|
|
1443
|
-
"Everframe.rn",
|
|
1444
|
-
"companion submitProvider threw: ${t.javaClass.simpleName}: ${t.message}",
|
|
1445
|
-
)
|
|
1446
|
-
return@work CompanionCaptureBridge.SubmitResult.Err("ingest_error")
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
when (result) {
|
|
1450
|
-
is ReportResult.Submitted ->
|
|
1451
|
-
CompanionCaptureBridge.SubmitResult.Ok(result.reportId.toString())
|
|
1452
|
-
is ReportResult.Queued ->
|
|
1453
|
-
CompanionCaptureBridge.SubmitResult.Ok(result.reportId.toString())
|
|
1454
|
-
is ReportResult.Cancelled ->
|
|
1455
|
-
CompanionCaptureBridge.SubmitResult.Err(result.reason)
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// ----- __previewProvider (Task 11b) -----
|
|
1461
|
-
// The 2fps preview loop's seam — low-res JPEG for the live-preview
|
|
1462
|
-
// stream (~854px longest edge, quality ~60, see
|
|
1463
|
-
// ScreenshotCapture.capturePreviewFrame's own defaults). Resolves
|
|
1464
|
-
// the current Activity exactly like __captureProvider above; no
|
|
1465
|
-
// foreground Activity (or a zero-size window) degrades to null,
|
|
1466
|
-
// which CompanionPreviewSession's loop maps to `capture_unavailable`
|
|
1467
|
-
// and stops — never a crash, never a throw (DEFE-02).
|
|
1468
|
-
CompanionCaptureBridge.__previewProvider = previewProvider@{
|
|
1469
|
-
val activity = reactContext.currentActivity ?: return@previewProvider null
|
|
1470
|
-
val rects = SensitiveRectRegistry.collectInWindowCoords(activity)
|
|
1471
|
-
ScreenshotCapture.capturePreviewFrame(activity, rects)
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
// ----- __shotCaptureProvider (Task 11b) -----
|
|
1475
|
-
// The shot-stash seam — `shot.request`'s contract is "capture fresh
|
|
1476
|
-
// now, at REPORT grade", which a preview-grade frame does not satisfy.
|
|
1477
|
-
// `captureBeforeReporter` is reused rather than duplicated, so a shot
|
|
1478
|
-
// is shaped exactly like the report's own screenshot: PNG, and
|
|
1479
|
-
// downscaled to `ScreenshotCapture.MAX_EDGE_PX` (2048) on the longest
|
|
1480
|
-
// edge. `mime` reflects the actual encode instead of asserting a
|
|
1481
|
-
// format the bytes don't carry.
|
|
1482
|
-
//
|
|
1483
|
-
// Task 11b review, finding 4: this comment used to claim "full
|
|
1484
|
-
// resolution" three times over while routing through a 2048-capped
|
|
1485
|
-
// path. The cap is deliberate and stays — a shot is evidence attached
|
|
1486
|
-
// to a report, and matching the primary screenshot's shaping is the
|
|
1487
|
-
// point — but the claim was simply false, and the next person to port
|
|
1488
|
-
// this (iOS) would have built an uncapped path to satisfy it.
|
|
1489
|
-
CompanionCaptureBridge.__shotCaptureProvider = shotProvider@{
|
|
1490
|
-
val activity = reactContext.currentActivity ?: return@shotProvider null
|
|
1491
|
-
val rects = SensitiveRectRegistry.collectInWindowCoords(activity)
|
|
1492
|
-
val capture = ScreenshotCapture.captureBeforeReporter(activity, rects)
|
|
1493
|
-
?: return@shotProvider null
|
|
1494
|
-
// Task 11b review, finding 2: `CaptureResult` hands us ownership of
|
|
1495
|
-
// this bitmap and only `pngBytes` travels onward, so it is released
|
|
1496
|
-
// here rather than left for the GC — ~7.5 MB per shot otherwise.
|
|
1497
|
-
capture.bitmap.recycle()
|
|
1498
|
-
PreviewCapture(
|
|
1499
|
-
bytes = capture.pngBytes,
|
|
1500
|
-
width = capture.widthPx,
|
|
1501
|
-
height = capture.heightPx,
|
|
1502
|
-
mime = "image/png",
|
|
1503
|
-
)
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
// ----- CompanionBadge.__activityProvider (Task 6, naming spec 2026-08-24) -----
|
|
1507
|
-
// Same seam, same reasoning as every provider above: `:everframe-core`
|
|
1508
|
-
// is a plain AAR and cannot resolve a foreground Activity on its own.
|
|
1509
|
-
// Mirrors the removed `CompanionSharingIndicator.__activityProvider`
|
|
1510
|
-
// install site this task's brief pointed at — resolved fresh on every
|
|
1511
|
-
// call (never cached), exactly like `reactContext.currentActivity`
|
|
1512
|
-
// is read fresh above for capture/preview/shot.
|
|
1513
|
-
CompanionBadge.__activityProvider = { reactContext.currentActivity }
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
/**
|
|
1517
|
-
* Helper — minimal AssembledPayload for the degraded-path. The bridge
|
|
1518
|
-
* accepts a payload with empty pngBytes; the phone reporter SPA shows
|
|
1519
|
-
* "0 bytes" in the assembled state and the user can still tap Send.
|
|
1520
|
-
*/
|
|
1521
|
-
private fun emptyAssembledPayload(correlationId: String): CompanionCaptureBridge.AssembledPayload {
|
|
1522
|
-
val toggles = ReportAssembledToggles(
|
|
1523
|
-
logs = false, metadata = true, network = false, screenshot = false, uiTree = false,
|
|
1524
|
-
)
|
|
1525
|
-
val counts = ReportAssembledCounts(breadcrumbs = null, logs = 0L, network = 0L, uiTreeNodes = 0L)
|
|
1526
|
-
val assembled = ReportAssembled(
|
|
1527
|
-
correlationId = correlationId,
|
|
1528
|
-
counts = counts,
|
|
1529
|
-
mime = "image/png",
|
|
1530
|
-
size = 0L,
|
|
1531
|
-
toggles = toggles,
|
|
1532
|
-
)
|
|
1533
|
-
return CompanionCaptureBridge.AssembledPayload(
|
|
1534
|
-
assembled = assembled,
|
|
1535
|
-
pngBytes = ByteArray(0),
|
|
1536
|
-
)
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
/**
|
|
1540
|
-
* Re-encode the captured screenshot for the relay hop. Drops the
|
|
1541
|
-
* envelope PNG fidelity (we keep `capture.pngBytes` separately for the
|
|
1542
|
-
* eventual submit) in exchange for ~60-80% byte reduction on the
|
|
1543
|
-
* phone-bound binary frame.
|
|
1544
|
-
*
|
|
1545
|
-
* Format preference, in order:
|
|
1546
|
-
* 1. WEBP_LOSSY @ q=85 — API 30+; ~70-80% byte cut, no visible loss
|
|
1547
|
-
* for screenshot-class images.
|
|
1548
|
-
* 2. WEBP (legacy enum, behaves as lossy when quality < 100) —
|
|
1549
|
-
* API 24-29. Same wire mime; encoder is older but bytes still
|
|
1550
|
-
* decode on every phone.
|
|
1551
|
-
* 3. JPEG @ q=85 — universal fallback, ~50-60% byte cut.
|
|
1552
|
-
*
|
|
1553
|
-
* Returns the original PNG bytes unchanged when every encoder fails
|
|
1554
|
-
* (DEFE-02: don't block the report flow on a re-encode failure).
|
|
1555
|
-
*/
|
|
1556
|
-
private fun reencodeScreenshot(
|
|
1557
|
-
bitmap: android.graphics.Bitmap,
|
|
1558
|
-
fallbackPng: ByteArray,
|
|
1559
|
-
): Pair<ByteArray, String> {
|
|
1560
|
-
// Try WEBP_LOSSY (API 30+), then legacy WEBP, then JPEG.
|
|
1561
|
-
if (android.os.Build.VERSION.SDK_INT >= 30) {
|
|
1562
|
-
val webp = runCatching {
|
|
1563
|
-
val out = java.io.ByteArrayOutputStream()
|
|
1564
|
-
@Suppress("DEPRECATION_ERROR")
|
|
1565
|
-
// CompressFormat.WEBP_LOSSY introduced in API 30.
|
|
1566
|
-
val ok = bitmap.compress(
|
|
1567
|
-
android.graphics.Bitmap.CompressFormat.WEBP_LOSSY, 85, out,
|
|
1568
|
-
)
|
|
1569
|
-
if (ok) out.toByteArray() else null
|
|
1570
|
-
}.getOrNull()
|
|
1571
|
-
if (webp != null && webp.size < fallbackPng.size) return Pair(webp, "image/webp")
|
|
1572
|
-
}
|
|
1573
|
-
val legacyWebp = runCatching {
|
|
1574
|
-
val out = java.io.ByteArrayOutputStream()
|
|
1575
|
-
@Suppress("DEPRECATION")
|
|
1576
|
-
val ok = bitmap.compress(android.graphics.Bitmap.CompressFormat.WEBP, 85, out)
|
|
1577
|
-
if (ok) out.toByteArray() else null
|
|
1578
|
-
}.getOrNull()
|
|
1579
|
-
if (legacyWebp != null && legacyWebp.size < fallbackPng.size) {
|
|
1580
|
-
return Pair(legacyWebp, "image/webp")
|
|
1581
|
-
}
|
|
1582
|
-
val jpeg = runCatching {
|
|
1583
|
-
val out = java.io.ByteArrayOutputStream()
|
|
1584
|
-
val ok = bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 85, out)
|
|
1585
|
-
if (ok) out.toByteArray() else null
|
|
1586
|
-
}.getOrNull()
|
|
1587
|
-
if (jpeg != null && jpeg.size < fallbackPng.size) return Pair(jpeg, "image/jpeg")
|
|
1588
|
-
return Pair(fallbackPng, "image/png")
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
/** Test seam — assert provider install/uninstall via the bridge. */
|
|
1592
|
-
internal fun hasCompanionProvidersInstalledForTesting(): Boolean =
|
|
1593
|
-
CompanionCaptureBridge.__captureProvider != null &&
|
|
1594
|
-
CompanionCaptureBridge.__submitProvider != null
|
|
1595
|
-
|
|
1596
777
|
@ReactMethod
|
|
1597
778
|
override fun addListener(eventName: String) {
|
|
1598
779
|
// No-op — RN NativeEventEmitter contract. RCTDeviceEventEmitter on
|
|
1599
780
|
// the JS side handles subscriber bookkeeping internally; we only
|
|
1600
781
|
// need to ensure the method is declared so RN's warning suppression
|
|
1601
|
-
// is satisfied.
|
|
1602
|
-
// startCompanion(), not on first addListener().
|
|
782
|
+
// is satisfied.
|
|
1603
783
|
}
|
|
1604
784
|
|
|
1605
785
|
@ReactMethod
|
|
@@ -1607,46 +787,6 @@ class EverframeModule(
|
|
|
1607
787
|
// No-op — see addListener() comment.
|
|
1608
788
|
}
|
|
1609
789
|
|
|
1610
|
-
/**
|
|
1611
|
-
* Called by the JS companion module from the `everframe.companion
|
|
1612
|
-
* .reportRequested` event handler once it is ready for capture to
|
|
1613
|
-
* proceed. Releases the `awaitJsReactTreeAttach` waiter for this correlation id so the
|
|
1614
|
-
* capture provider can proceed. Idempotent — late signals after the
|
|
1615
|
-
* timeout fired are harmless.
|
|
1616
|
-
*/
|
|
1617
|
-
@ReactMethod
|
|
1618
|
-
override fun signalCompanionReportRequestReady(correlationId: String) {
|
|
1619
|
-
pendingReactTreeAttachSignals.remove(correlationId)?.complete(Unit)
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
/** Per-correlation-id signals released by `signalCompanionReportRequestReady`. */
|
|
1623
|
-
private val pendingReactTreeAttachSignals =
|
|
1624
|
-
ConcurrentHashMap<String, kotlinx.coroutines.CompletableDeferred<Unit>>()
|
|
1625
|
-
|
|
1626
|
-
/**
|
|
1627
|
-
* Emit `everframe.companion.reportRequested` then block up to 250ms
|
|
1628
|
-
* waiting for JS to call back via `signalCompanionReportRequestReady`.
|
|
1629
|
-
* The wait is bounded so a non-responsive JS thread can't stall the
|
|
1630
|
-
* native capture path indefinitely (DEFE-02). Runs on the OkHttp WS
|
|
1631
|
-
* thread — the surrounding `runCatching` block in the capture provider
|
|
1632
|
-
* absorbs any throw.
|
|
1633
|
-
*
|
|
1634
|
-
* Returns silently when:
|
|
1635
|
-
* - JS signals ready (fast path, ~5-50ms on a healthy app)
|
|
1636
|
-
* - The 250ms timeout elapses (slow path)
|
|
1637
|
-
*/
|
|
1638
|
-
private fun awaitJsReactTreeAttach(correlationId: String) {
|
|
1639
|
-
val signal = kotlinx.coroutines.CompletableDeferred<Unit>()
|
|
1640
|
-
pendingReactTreeAttachSignals[correlationId] = signal
|
|
1641
|
-
val emitter = reactApplicationContext
|
|
1642
|
-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1643
|
-
emitter.emit(COMPANION_REPORT_REQUESTED_EVENT, correlationId)
|
|
1644
|
-
runBlocking {
|
|
1645
|
-
kotlinx.coroutines.withTimeoutOrNull(250) { signal.await() }
|
|
1646
|
-
}
|
|
1647
|
-
pendingReactTreeAttachSignals.remove(correlationId)
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
790
|
// ---------------- Extra-resolver ask-and-wait (spec 2026-09-17 setExtra-resolver) ----------------
|
|
1651
791
|
|
|
1652
792
|
/**
|
|
@@ -1655,8 +795,7 @@ class EverframeModule(
|
|
|
1655
795
|
* resolver is registered — pushed a freshly resolved value through the
|
|
1656
796
|
* ordinary `setExtra` method above. Releases `awaitJsExtraResolve`'s
|
|
1657
797
|
* bounded wait for this correlation id. Idempotent — a late signal
|
|
1658
|
-
* after the 250ms timeout already fired is a harmless no-op
|
|
1659
|
-
* like `signalCompanionReportRequestReady` above.
|
|
798
|
+
* after the 250ms timeout already fired is a harmless no-op.
|
|
1660
799
|
*/
|
|
1661
800
|
@ReactMethod
|
|
1662
801
|
override fun signalExtraResolverReady(correlationId: String) {
|
|
@@ -1677,10 +816,9 @@ class EverframeModule(
|
|
|
1677
816
|
* before this feature landed (requirement: "no behaviour change
|
|
1678
817
|
* whatsoever").
|
|
1679
818
|
*
|
|
1680
|
-
* Otherwise
|
|
1681
|
-
*
|
|
1682
|
-
*
|
|
1683
|
-
* `awaitJsReactTreeAttach`: this hook's caller
|
|
819
|
+
* Otherwise emit `everframe.extra.resolveRequested`, then suspend up to
|
|
820
|
+
* 250ms for JS's `signalExtraResolverReady` ack. `suspend`: this hook's
|
|
821
|
+
* caller
|
|
1684
822
|
* (`Everframe.consumePendingAttachments()`) may run on Android's Main
|
|
1685
823
|
* dispatcher (RN's `openReporter()`/native shake both funnel through
|
|
1686
824
|
* `Everframe.report.open()` there), so a real suspend point is required
|
|
@@ -1702,90 +840,6 @@ class EverframeModule(
|
|
|
1702
840
|
}
|
|
1703
841
|
}
|
|
1704
842
|
|
|
1705
|
-
/**
|
|
1706
|
-
* Install StateFlow collectors on `Everframe.companion.state`, `.pairUrl`,
|
|
1707
|
-
* `.code` and `.attachedUserName` that emit the matching
|
|
1708
|
-
* `everframe.companion.*` device events. Idempotent — the
|
|
1709
|
-
* `companionCollectorsInstalled` latch survives stop/start cycles because
|
|
1710
|
-
* the underlying StateFlows on `Everframe.companion` outlive the
|
|
1711
|
-
* RelayWSClient.
|
|
1712
|
-
*/
|
|
1713
|
-
private fun installCompanionCollectorsIfNeeded() {
|
|
1714
|
-
if (companionCollectorsInstalled) return
|
|
1715
|
-
companionCollectorsInstalled = true
|
|
1716
|
-
|
|
1717
|
-
val emitter = reactApplicationContext
|
|
1718
|
-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1719
|
-
|
|
1720
|
-
companionStateJob = companionScope.launch {
|
|
1721
|
-
Everframe.companion.state.collect { state ->
|
|
1722
|
-
android.util.Log.i("Everframe.companion", "emit state=$state")
|
|
1723
|
-
emitter.emit(COMPANION_STATE_EVENT, state.toRnString())
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
companionPairUrlJob = companionScope.launch {
|
|
1727
|
-
Everframe.companion.pairUrl.collect { url ->
|
|
1728
|
-
android.util.Log.i("Everframe.companion", "emit pairUrl=${if (url == null) "null" else "set"}")
|
|
1729
|
-
// Both null and String cross to JS cleanly via the
|
|
1730
|
-
// DeviceEventManager — null becomes `null` on the JS side.
|
|
1731
|
-
emitter.emit(COMPANION_PAIR_URL_EVENT, url)
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
// Companion discovery (spec 2026-08-07). Same install-once lifecycle as
|
|
1735
|
-
// the two above, and for the same reason: `Everframe.companion.code`
|
|
1736
|
-
// is a StateFlow that outlives the RelayWSClient, so a collector
|
|
1737
|
-
// installed on the FIRST start replays the current value and keeps
|
|
1738
|
-
// delivering across stop/start cycles. Re-installing per start would
|
|
1739
|
-
// race the initial emission and lose it.
|
|
1740
|
-
companionCodeJob = companionScope.launch {
|
|
1741
|
-
Everframe.companion.code.collect { code ->
|
|
1742
|
-
android.util.Log.i("Everframe.companion", "emit code=${if (code == null) "null" else "set"}")
|
|
1743
|
-
emitter.emit(COMPANION_CODE_EVENT, code)
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
companionAttachedUserNameJob = companionScope.launch {
|
|
1747
|
-
Everframe.companion.attachedUserName.collect { name ->
|
|
1748
|
-
// The NAME itself is not logged — it is a dashboard member's
|
|
1749
|
-
// display name, and this log line goes to logcat.
|
|
1750
|
-
android.util.Log.i(
|
|
1751
|
-
"Everframe.companion",
|
|
1752
|
-
"emit attachedUserName=${if (name == null) "null" else "set"}",
|
|
1753
|
-
)
|
|
1754
|
-
emitter.emit(COMPANION_ATTACHED_USER_NAME_EVENT, name)
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
// Resolved device display name (naming spec 2026-08-24). Same
|
|
1758
|
-
// install-once lifecycle as the collectors above, and for the same
|
|
1759
|
-
// reason: the StateFlow outlives the RelayWSClient, so a collector
|
|
1760
|
-
// installed on the FIRST start replays the current value across
|
|
1761
|
-
// stop/start cycles.
|
|
1762
|
-
companionResolvedNameJob = companionScope.launch {
|
|
1763
|
-
Everframe.companion.resolvedName.collect { name ->
|
|
1764
|
-
android.util.Log.i(
|
|
1765
|
-
"Everframe.companion",
|
|
1766
|
-
"emit resolvedName=${if (name == null) "null" else "set"}",
|
|
1767
|
-
)
|
|
1768
|
-
emitter.emit(COMPANION_RESOLVED_NAME_EVENT, name)
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
// Attach-PIN challenge (spec 2026-08-19). Same install-once lifecycle
|
|
1772
|
-
// as the collectors above, and for the same reason: the StateFlow
|
|
1773
|
-
// outlives the RelayWSClient, so a collector installed on the FIRST
|
|
1774
|
-
// start replays the current value across stop/start cycles. Payload
|
|
1775
|
-
// conversion lives in `AttachChallengeInfo.toWritableMap()` below so
|
|
1776
|
-
// this collector body stays as short as its siblings.
|
|
1777
|
-
companionAttachChallengeJob = companionScope.launch {
|
|
1778
|
-
Everframe.companion.attachChallenge.collect { challenge ->
|
|
1779
|
-
// SECURITY: the PIN code itself is not logged.
|
|
1780
|
-
android.util.Log.i(
|
|
1781
|
-
"Everframe.companion",
|
|
1782
|
-
"emit attachChallenge=${if (challenge == null) "null" else "set"}",
|
|
1783
|
-
)
|
|
1784
|
-
emitter.emit(COMPANION_ATTACH_CHALLENGE_EVENT, challenge?.toWritableMap())
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
843
|
// ---------------- Helpers ----------------
|
|
1790
844
|
|
|
1791
845
|
|
|
@@ -1818,7 +872,7 @@ class EverframeModule(
|
|
|
1818
872
|
}
|
|
1819
873
|
}
|
|
1820
874
|
|
|
1821
|
-
// rewriteEmulatorLoopback removed — JS-side `apiBase`
|
|
1822
|
-
//
|
|
875
|
+
// rewriteEmulatorLoopback removed — JS-side `apiBase` endpoint args are
|
|
876
|
+
// no longer plumbed through. The 10.0.2.2 emulator
|
|
1823
877
|
// alias is baked into the debug-variant BuildConfig.INGEST_URL fallback.
|
|
1824
878
|
}
|