@mentra/engine 3.2.0-dev.195 → 3.2.0-dev.206
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/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +3 -7
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +7 -20
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +16 -99
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/asg/galleryNotices.d.ts +1 -1
- package/build/services/asg/galleryNotices.d.ts.map +1 -1
- package/build/services/asg/galleryNotices.js.map +1 -1
- package/build/services/asg/gallerySyncService.d.ts +14 -0
- package/build/services/asg/gallerySyncService.d.ts.map +1 -1
- package/build/services/asg/gallerySyncService.js +232 -96
- package/build/services/asg/gallerySyncService.js.map +1 -1
- package/build/services/slimStreamStatus.d.ts.map +1 -1
- package/build/services/slimStreamStatus.js +16 -0
- package/build/services/slimStreamStatus.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/DeviceEventRouter.ts +5 -9
- package/src/services/PhoneStreamCoordinator.ts +23 -128
- package/src/services/asg/galleryNotices.ts +1 -0
- package/src/services/asg/gallerySyncService.ts +260 -112
- package/src/services/slimStreamStatus.ts +10 -1
- package/build/services/StreamLifecycleController.d.ts +0 -85
- package/build/services/StreamLifecycleController.d.ts.map +0 -1
- package/build/services/StreamLifecycleController.js +0 -173
- package/build/services/StreamLifecycleController.js.map +0 -1
- package/src/services/StreamLifecycleController.ts +0 -243
|
@@ -29,6 +29,62 @@ import { validateCaptureMetadataForDownload } from "./galleryMediaValidation";
|
|
|
29
29
|
import { emitGalleryNotice } from "./galleryNotices";
|
|
30
30
|
import { galleryTransferLedger } from "./galleryTransferLedger";
|
|
31
31
|
import { cameraRollExportCoordinator } from "./cameraRollExportCoordinator";
|
|
32
|
+
/**
|
|
33
|
+
* react-native-wifi-reborn rejects SSID reads with a stable `code` from its CONNECT_ERRORS
|
|
34
|
+
* enum. Three DIFFERENT prose messages map to the same "this install will never be allowed
|
|
35
|
+
* to read the SSID" wall — "Cannot detect SSID because LocationPermission is Denied",
|
|
36
|
+
* "...is Restricted", and the bare "Permission not granted" from the not-determined path —
|
|
37
|
+
* so match the code, never the text.
|
|
38
|
+
*/
|
|
39
|
+
const SSID_PERMISSION_ERROR_CODES = new Set([
|
|
40
|
+
"locationPermissionDenied",
|
|
41
|
+
"locationPermissionRestricted",
|
|
42
|
+
"locationPermissionMissing",
|
|
43
|
+
]);
|
|
44
|
+
export function isSsidPermissionError(error) {
|
|
45
|
+
const code = error?.code;
|
|
46
|
+
// A bridged `code` is authoritative: `couldNotDetectSSID` is a transient read failure,
|
|
47
|
+
// NOT a permission wall, and must keep polling.
|
|
48
|
+
if (typeof code === "string")
|
|
49
|
+
return SSID_PERMISSION_ERROR_CODES.has(code);
|
|
50
|
+
// Fallback for errors that reach us with only a message (older bridges, test doubles).
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
return /location\s*permission/i.test(message) || /permission not granted/i.test(message);
|
|
53
|
+
}
|
|
54
|
+
export async function verifyIosHotspotSsid(targetSsid, readCurrentSsid, sleep, maxAttempts = 30) {
|
|
55
|
+
let lastSeenSsid = "unknown";
|
|
56
|
+
let observedSsid = false;
|
|
57
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
58
|
+
try {
|
|
59
|
+
const currentSsid = await readCurrentSsid(attempt + 1);
|
|
60
|
+
// Only a NON-EMPTY read proves we can see the network. An empty SSID is an
|
|
61
|
+
// "unknown", not a mismatch — reporting it as a mismatch would hard-fail the sync
|
|
62
|
+
// for the same reason this fallback exists.
|
|
63
|
+
if (currentSsid) {
|
|
64
|
+
observedSsid = true;
|
|
65
|
+
lastSeenSsid = currentSsid;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lastSeenSsid = "null";
|
|
69
|
+
}
|
|
70
|
+
if (currentSsid && currentSsid === targetSsid) {
|
|
71
|
+
return { status: "matched", lastSeenSsid, permissionBlocked: false };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
lastSeenSsid = "error";
|
|
76
|
+
if (isSsidPermissionError(error)) {
|
|
77
|
+
return { status: "unavailable", lastSeenSsid, permissionBlocked: true };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (attempt < maxAttempts - 1)
|
|
81
|
+
await sleep();
|
|
82
|
+
}
|
|
83
|
+
// Falling out of the loop without a single legible SSID is "we never saw the network",
|
|
84
|
+
// NOT "we are not allowed to look" — transient read failures and empty reads land here
|
|
85
|
+
// with Location granted, so permissionBlocked stays false.
|
|
86
|
+
return { status: observedSsid ? "mismatched" : "unavailable", lastSeenSsid, permissionBlocked: false };
|
|
87
|
+
}
|
|
32
88
|
// Timing constants
|
|
33
89
|
const TIMING = {
|
|
34
90
|
HOTSPOT_CONNECT_DELAY_MS: 3000, // Increased from 1000ms - hotspot needs time to broadcast and become discoverable
|
|
@@ -62,6 +118,11 @@ class GallerySyncService {
|
|
|
62
118
|
wifiSettingsOpenedAt = null; // Timestamp when user was sent to WiFi settings
|
|
63
119
|
syncStartPromise = null;
|
|
64
120
|
startAborted = false;
|
|
121
|
+
// Authoritative answer to "can this run read the phone's WiFi SSID?", captured from the
|
|
122
|
+
// Location permission gate in pre-flight. Every SSID comparison below is advisory only:
|
|
123
|
+
// when this is false we skip the read entirely and lean on the glasses connectivity
|
|
124
|
+
// probe instead of failing the sync. Assume readable until pre-flight says otherwise.
|
|
125
|
+
ssidReadable = true;
|
|
65
126
|
constructor() { }
|
|
66
127
|
static getInstance() {
|
|
67
128
|
if (!GallerySyncService.instance) {
|
|
@@ -128,6 +189,7 @@ class GallerySyncService {
|
|
|
128
189
|
this.waitingForWifiRetry = false;
|
|
129
190
|
this.wifiSettingsOpenedAt = null;
|
|
130
191
|
this.startAborted = false;
|
|
192
|
+
this.ssidReadable = true;
|
|
131
193
|
this.isInitialized = false;
|
|
132
194
|
console.log("[GallerySyncService] Cleaned up");
|
|
133
195
|
}
|
|
@@ -431,21 +493,25 @@ class GallerySyncService {
|
|
|
431
493
|
return;
|
|
432
494
|
}
|
|
433
495
|
console.log("[GallerySyncService] ✅ Notification permission handled");
|
|
434
|
-
// 2. Location permission (required to read WiFi SSID for hotspot verification)
|
|
496
|
+
// 2. Location permission (required to read WiFi SSID for hotspot verification).
|
|
497
|
+
// Sync does NOT block on this: record the answer once, here, and let every downstream
|
|
498
|
+
// SSID check consult `this.ssidReadable` instead of re-deriving it from a native error.
|
|
435
499
|
console.log("[GallerySyncService] 📍 Checking location permission...");
|
|
436
500
|
const hasLocationPermission = await permissions.check(PermissionFeatures.LOCATION);
|
|
437
501
|
if (!hasLocationPermission) {
|
|
438
502
|
console.log("[GallerySyncService] ⚠️ Location permission not granted - requesting...");
|
|
439
503
|
const granted = await permissions.request(PermissionFeatures.LOCATION);
|
|
504
|
+
this.ssidReadable = granted;
|
|
440
505
|
if (!granted) {
|
|
441
|
-
console.warn("[GallerySyncService] ❌ Location permission denied -
|
|
442
|
-
|
|
506
|
+
console.warn("[GallerySyncService] ❌ Location permission denied - skipping all SSID verification");
|
|
507
|
+
console.warn("[GallerySyncService] ➡️ Falling back to the glasses connectivity probe");
|
|
443
508
|
}
|
|
444
509
|
else {
|
|
445
510
|
console.log("[GallerySyncService] ✅ Location permission granted");
|
|
446
511
|
}
|
|
447
512
|
}
|
|
448
513
|
else {
|
|
514
|
+
this.ssidReadable = true;
|
|
449
515
|
console.log("[GallerySyncService] ✅ Location permission already granted");
|
|
450
516
|
}
|
|
451
517
|
if (this.shouldAbortPreFlight()) {
|
|
@@ -635,36 +701,54 @@ class GallerySyncService {
|
|
|
635
701
|
}
|
|
636
702
|
: null;
|
|
637
703
|
let isAlreadyConnected = false;
|
|
704
|
+
// Distinct from `!isAlreadyConnected`: we could not READ the SSID, so we know nothing
|
|
705
|
+
// about which network the phone is on. Re-requesting the hotspot in that state costs a
|
|
706
|
+
// pointless BLE round-trip on every sync, so route straight to the connect path instead.
|
|
707
|
+
let hotspotMembershipUnknown = false;
|
|
638
708
|
if (currentGlassesHotspot) {
|
|
639
709
|
console.log("[GallerySyncService] 📊 Glasses hotspot status:");
|
|
640
710
|
console.log("[GallerySyncService] - Enabled: true");
|
|
641
711
|
console.log(`[GallerySyncService] - SSID: ${currentGlassesHotspot.ssid}`);
|
|
642
712
|
console.log(`[GallerySyncService] - IP: ${currentGlassesHotspot.ip}`);
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
console.log(`[GallerySyncService]
|
|
657
|
-
|
|
713
|
+
if (!this.ssidReadable) {
|
|
714
|
+
console.log("[GallerySyncService] ⚠️ SSID unreadable (Location denied) - cannot tell if already joined");
|
|
715
|
+
console.log("[GallerySyncService] ➡️ Will attempt the glasses WiFi connection directly");
|
|
716
|
+
hotspotMembershipUnknown = true;
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
try {
|
|
720
|
+
const currentSSID = await WifiManager.getCurrentWifiSSID();
|
|
721
|
+
if (this.shouldAbortPreFlight()) {
|
|
722
|
+
console.log("[GallerySyncService] Pre-flight aborted after SSID check");
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
console.log(`[GallerySyncService] 📱 Phone current WiFi SSID: "${currentSSID}"`);
|
|
726
|
+
console.log(`[GallerySyncService] 🔍 Comparing with glasses hotspot SSID: "${currentGlassesHotspot.ssid}"`);
|
|
727
|
+
isAlreadyConnected = currentSSID === currentGlassesHotspot.ssid;
|
|
728
|
+
if (isAlreadyConnected) {
|
|
729
|
+
console.log("[GallerySyncService] ✅ Phone is already connected to glasses hotspot!");
|
|
730
|
+
}
|
|
731
|
+
else if (currentSSID) {
|
|
732
|
+
console.log(`[GallerySyncService] ⚠️ Phone is on different network (${currentSSID})`);
|
|
733
|
+
console.log("[GallerySyncService] ➡️ Will request hotspot connection");
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
console.log("[GallerySyncService] ⚠️ Phone not connected to any WiFi network");
|
|
737
|
+
}
|
|
658
738
|
}
|
|
659
|
-
|
|
660
|
-
console.
|
|
739
|
+
catch (error) {
|
|
740
|
+
console.warn("[GallerySyncService] ⚠️ Could not verify current WiFi SSID:", error);
|
|
741
|
+
isAlreadyConnected = false;
|
|
742
|
+
// Permission was granted at pre-flight but the read still hit a wall (revoked
|
|
743
|
+
// between the two, or a platform we mis-predicted). Latch it so the rest of this
|
|
744
|
+
// sync stops paying for reads that cannot succeed, and treat membership as
|
|
745
|
+
// unknown rather than "definitely not joined".
|
|
746
|
+
if (isSsidPermissionError(error)) {
|
|
747
|
+
this.ssidReadable = false;
|
|
748
|
+
hotspotMembershipUnknown = true;
|
|
749
|
+
}
|
|
661
750
|
}
|
|
662
751
|
}
|
|
663
|
-
catch (error) {
|
|
664
|
-
console.warn("[GallerySyncService] ⚠️ Could not verify current WiFi SSID:", error);
|
|
665
|
-
// If we can't verify, don't assume we're connected - request hotspot
|
|
666
|
-
isAlreadyConnected = false;
|
|
667
|
-
}
|
|
668
752
|
}
|
|
669
753
|
else {
|
|
670
754
|
console.log("[GallerySyncService] ℹ️ Glasses hotspot not currently enabled");
|
|
@@ -673,6 +757,17 @@ class GallerySyncService {
|
|
|
673
757
|
// Every fallible pre-flight gate has passed. It is now safe to replace the previous
|
|
674
758
|
// in-memory queue; startFileDownload immediately recovers its durable ledger work.
|
|
675
759
|
mediaProcessingQueue.reset();
|
|
760
|
+
// SSID unreadable + hotspot already up: connectToHotspotWifi handles BOTH "already
|
|
761
|
+
// joined" (the native connect resolves immediately) and "needs to join", and it still
|
|
762
|
+
// probes the glasses server before downloading. Skipping straight to startFileDownload
|
|
763
|
+
// would be unsafe here — nothing has proven connectivity yet.
|
|
764
|
+
if (hotspotMembershipUnknown && currentGlassesHotspot) {
|
|
765
|
+
const hotspotInfo = currentGlassesHotspot;
|
|
766
|
+
store.setHotspotInfo(hotspotInfo);
|
|
767
|
+
store.setSyncState("connecting_wifi");
|
|
768
|
+
await this.connectToHotspotWifi(hotspotInfo);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
676
771
|
if (isAlreadyConnected && currentGlassesHotspot) {
|
|
677
772
|
const hotspotInfo = currentGlassesHotspot;
|
|
678
773
|
store.setHotspotInfo(hotspotInfo);
|
|
@@ -811,28 +906,38 @@ class GallerySyncService {
|
|
|
811
906
|
console.log(`[GallerySyncService] 📡 ATTEMPT ${attempt}/${TIMING.IOS_WIFI_MAX_RETRIES} - Starting WiFi connection`);
|
|
812
907
|
console.log(`[GallerySyncService] ⏱️ Time since WiFi phase started: ${Date.now() - wifiConnectStartTime}ms`);
|
|
813
908
|
console.log(`[GallerySyncService] 📱 App backgrounded during connection: ${appBackgrounded}`);
|
|
814
|
-
// Check current WiFi state before attempting connection
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
console.log(
|
|
819
|
-
// Check if already connected (shouldn't happen, but good to verify)
|
|
820
|
-
if (!localNetworkTransport.supportsScopedConnection() && preConnectSSID === hotspotInfo.ssid) {
|
|
821
|
-
console.log("[GallerySyncService] ✅ Already connected to target SSID! Proceeding to download.");
|
|
822
|
-
appStateSubscription.remove();
|
|
823
|
-
const totalWifiDuration = Date.now() - wifiConnectStartTime;
|
|
824
|
-
console.log("[GallerySyncService] ========================================");
|
|
825
|
-
console.log("[GallerySyncService] ✅ WIFI CONNECTION COMPLETE (already connected)");
|
|
826
|
-
console.log("[GallerySyncService] ========================================");
|
|
827
|
-
console.log(`[GallerySyncService] ⏱️ Total WiFi phase duration: ${totalWifiDuration}ms`);
|
|
828
|
-
console.log(`[GallerySyncService] 🚀 Proceeding to file download from ${hotspotInfo.ip}:8089`);
|
|
829
|
-
await this.startFileDownload(hotspotInfo);
|
|
830
|
-
return; // Exit function successfully
|
|
831
|
-
}
|
|
909
|
+
// Check current WiFi state before attempting connection. This is a shortcut, not
|
|
910
|
+
// a gate — when the SSID is unreadable we simply fall through to the connect call,
|
|
911
|
+
// which is itself a no-op if the phone is already on the target network.
|
|
912
|
+
if (!this.ssidReadable) {
|
|
913
|
+
console.log("[GallerySyncService] 📡 Skipping pre-connect SSID read (Location denied)");
|
|
832
914
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
915
|
+
else {
|
|
916
|
+
try {
|
|
917
|
+
const preConnectSSID = await WifiManager.getCurrentWifiSSID();
|
|
918
|
+
console.log(`[GallerySyncService] 📡 Current WiFi SSID: "${preConnectSSID}"`);
|
|
919
|
+
// Check if already connected (shouldn't happen, but good to verify)
|
|
920
|
+
if (!localNetworkTransport.supportsScopedConnection() && preConnectSSID === hotspotInfo.ssid) {
|
|
921
|
+
console.log("[GallerySyncService] ✅ Already connected to target SSID! Proceeding to download.");
|
|
922
|
+
appStateSubscription.remove();
|
|
923
|
+
const totalWifiDuration = Date.now() - wifiConnectStartTime;
|
|
924
|
+
console.log("[GallerySyncService] ========================================");
|
|
925
|
+
console.log("[GallerySyncService] ✅ WIFI CONNECTION COMPLETE (already connected)");
|
|
926
|
+
console.log("[GallerySyncService] ========================================");
|
|
927
|
+
console.log(`[GallerySyncService] ⏱️ Total WiFi phase duration: ${totalWifiDuration}ms`);
|
|
928
|
+
console.log(`[GallerySyncService] 🚀 Proceeding to file download from ${hotspotInfo.ip}:8089`);
|
|
929
|
+
await this.startFileDownload(hotspotInfo);
|
|
930
|
+
return; // Exit function successfully
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
catch (preError) {
|
|
934
|
+
const message = preError instanceof Error ? preError.message : String(preError);
|
|
935
|
+
console.warn(`[GallerySyncService] ⚠️ Could not get current SSID: ${message}`);
|
|
936
|
+
console.warn("[GallerySyncService] ⚠️ Error code:", preError?.code);
|
|
937
|
+
// A permission wall here applies to every later read too — stop paying for it.
|
|
938
|
+
if (isSsidPermissionError(preError))
|
|
939
|
+
this.ssidReadable = false;
|
|
940
|
+
}
|
|
836
941
|
}
|
|
837
942
|
console.log(`[GallerySyncService] 🔌 Connecting the glasses-local transport...`);
|
|
838
943
|
console.log(`[GallerySyncService] 🔌 Parameters:`);
|
|
@@ -851,47 +956,53 @@ class GallerySyncService {
|
|
|
851
956
|
if (appBackgrounded && appBackgroundTime) {
|
|
852
957
|
console.log(`[GallerySyncService] ⏱️ Time until backgrounding: ${appBackgroundTime - connectCallStartTime}ms`);
|
|
853
958
|
}
|
|
854
|
-
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
|
|
959
|
+
// NOTE: react-native-wifi-reborn >= 4.x already polls the SSID natively inside
|
|
960
|
+
// connectToProtectedSSID (up to 20 x 500ms) and only resolves once it observes the
|
|
961
|
+
// target network, so reaching this line is itself decent evidence of a real join.
|
|
962
|
+
// The poll below is a second opinion, not the primary gate — never fail on it
|
|
963
|
+
// when the SSID simply cannot be read.
|
|
964
|
+
if (Platform.OS === "ios" && !this.ssidReadable) {
|
|
965
|
+
console.log(`[GallerySyncService] 🍎 Skipping SSID verification (Location denied) - using glasses connectivity probe`);
|
|
966
|
+
}
|
|
967
|
+
else if (Platform.OS === "ios") {
|
|
858
968
|
console.log(`[GallerySyncService] 🍎 iOS: Starting connection verification...`);
|
|
859
969
|
console.log(`[GallerySyncService] 🍎 Will poll getCurrentWifiSSID() for up to 15 seconds`);
|
|
860
970
|
const maxVerifyAttempts = 30; // 30 × 500ms = 15 seconds
|
|
861
|
-
|
|
862
|
-
let lastSeenSSID = "unknown";
|
|
863
|
-
for (let i = 0; i < maxVerifyAttempts; i++) {
|
|
971
|
+
const verification = await verifyIosHotspotSsid(hotspotInfo.ssid, async (pollNumber) => {
|
|
864
972
|
try {
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (currentSSID === hotspotInfo.ssid) {
|
|
869
|
-
console.log(`[GallerySyncService] 🍎 ✅ VERIFICATION SUCCESS! Connected to target network after ${(i + 1) * 500}ms`);
|
|
870
|
-
connected = true;
|
|
871
|
-
break;
|
|
872
|
-
}
|
|
873
|
-
else if (i === 0 && currentSSID === lastSeenSSID) {
|
|
874
|
-
console.log(`[GallerySyncService] 🍎 ⚠️ Still on original network - iOS dialog may not have appeared yet`);
|
|
875
|
-
}
|
|
973
|
+
const currentSsid = await WifiManager.getCurrentWifiSSID();
|
|
974
|
+
console.log(`[GallerySyncService] 🍎 Verify poll ${pollNumber}/${maxVerifyAttempts}: Current="${currentSsid}", Target="${hotspotInfo.ssid}"`);
|
|
975
|
+
return currentSsid;
|
|
876
976
|
}
|
|
877
|
-
catch (
|
|
878
|
-
|
|
879
|
-
|
|
977
|
+
catch (error) {
|
|
978
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
979
|
+
console.log(`[GallerySyncService] 🍎 ⚠️ Poll ${pollNumber}: Could not check SSID: ${message}`);
|
|
980
|
+
throw error;
|
|
880
981
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
await new Promise((resolve) => BgTimer.setTimeout(() => resolve(), 500));
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
if (!connected) {
|
|
982
|
+
}, () => new Promise((resolve) => BgTimer.setTimeout(() => resolve(), 500)), maxVerifyAttempts);
|
|
983
|
+
if (verification.status === "mismatched") {
|
|
887
984
|
console.error(`[GallerySyncService] 🍎 ❌ VERIFICATION FAILED after 15 seconds`);
|
|
888
|
-
console.error(`[GallerySyncService] 🍎 Last seen SSID: "${
|
|
985
|
+
console.error(`[GallerySyncService] 🍎 Last seen SSID: "${verification.lastSeenSsid}"`);
|
|
889
986
|
console.error(`[GallerySyncService] 🍎 Expected SSID: "${hotspotInfo.ssid}"`);
|
|
890
987
|
console.error(`[GallerySyncService] 🍎 Possible causes:`);
|
|
891
988
|
console.error(`[GallerySyncService] 🍎 1. User did not tap "Join" on iOS WiFi dialog`);
|
|
892
|
-
console.error(`[GallerySyncService] 🍎 2. iOS dialog did not appear
|
|
989
|
+
console.error(`[GallerySyncService] 🍎 2. iOS dialog did not appear`);
|
|
893
990
|
console.error(`[GallerySyncService] 🍎 3. iOS refused to switch networks`);
|
|
894
|
-
throw new Error(`iOS WiFi verification failed - still on "${
|
|
991
|
+
throw new Error(`iOS WiFi verification failed - still on "${verification.lastSeenSsid}", expected "${hotspotInfo.ssid}"`);
|
|
992
|
+
}
|
|
993
|
+
if (verification.status === "unavailable") {
|
|
994
|
+
console.warn(`[GallerySyncService] 🍎 SSID inspection unavailable; using glasses connectivity probe instead`);
|
|
995
|
+
// Latch ONLY on a real permission wall (revoked between pre-flight and now).
|
|
996
|
+
// A run of transient or empty reads must leave `ssidReadable` alone: killing
|
|
997
|
+
// it here would disable the readable-mismatch gate for the remaining retries
|
|
998
|
+
// and make a later failure blame Location while it is granted.
|
|
999
|
+
if (verification.permissionBlocked) {
|
|
1000
|
+
console.warn(`[GallerySyncService] 🍎 Location permission was revoked mid-sync - skipping SSID gates`);
|
|
1001
|
+
this.ssidReadable = false;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
console.log(`[GallerySyncService] 🍎 ✅ SSID verification succeeded`);
|
|
895
1006
|
}
|
|
896
1007
|
}
|
|
897
1008
|
const attemptDuration = Date.now() - attemptStartTime;
|
|
@@ -901,25 +1012,36 @@ class GallerySyncService {
|
|
|
901
1012
|
// Remove app state listener
|
|
902
1013
|
appStateSubscription.remove();
|
|
903
1014
|
console.log("[GallerySyncService] 👂 App state listener removed");
|
|
904
|
-
// Final verification: Check SSID one more time before starting download
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1015
|
+
// Final verification: Check SSID one more time before starting download.
|
|
1016
|
+
// Only the READ is allowed to fail softly. A readable SSID that does not match is
|
|
1017
|
+
// a real "we are about to download from the wrong network" signal and must abort —
|
|
1018
|
+
// previously the throw was inside the try, so its own catch swallowed it and the
|
|
1019
|
+
// gate never actually stopped anything.
|
|
1020
|
+
let finalSSID = null;
|
|
1021
|
+
if (!this.ssidReadable) {
|
|
1022
|
+
console.log("[GallerySyncService] 📶 Skipping final SSID check (Location denied)");
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
try {
|
|
1026
|
+
finalSSID = localNetworkTransport.isScopedConnectionActive()
|
|
1027
|
+
? hotspotInfo.ssid
|
|
1028
|
+
: await WifiManager.getCurrentWifiSSID();
|
|
1029
|
+
console.log(`[GallerySyncService] 📶 Final SSID check before download: "${finalSSID}"`);
|
|
914
1030
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1031
|
+
catch (finalError) {
|
|
1032
|
+
const message = finalError instanceof Error ? finalError.message : String(finalError);
|
|
1033
|
+
console.warn(`[GallerySyncService] ⚠️ Could not perform final SSID check: ${message}`);
|
|
1034
|
+
// Continue anyway - we've done our best to verify
|
|
918
1035
|
}
|
|
919
1036
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1037
|
+
if (Platform.OS === "android") {
|
|
1038
|
+
// Some local builds can have stale generated typings for the Bluetooth SDK module.
|
|
1039
|
+
;
|
|
1040
|
+
BluetoothSdk.logCurrentWifiFrequency?.();
|
|
1041
|
+
}
|
|
1042
|
+
if (finalSSID && finalSSID !== hotspotInfo.ssid) {
|
|
1043
|
+
console.error(`[GallerySyncService] ❌ SSID mismatch detected! Expected "${hotspotInfo.ssid}", got "${finalSSID}"`);
|
|
1044
|
+
throw new Error(`WiFi SSID mismatch - connected to "${finalSSID}" instead of "${hotspotInfo.ssid}"`);
|
|
923
1045
|
}
|
|
924
1046
|
// iOS-specific: Wait for actual network connectivity to glasses
|
|
925
1047
|
// Even though SSID is correct, iOS needs time for routing tables to update
|
|
@@ -930,17 +1052,20 @@ class GallerySyncService {
|
|
|
930
1052
|
const maxProbeAttempts = 20; // 20 attempts × 500ms = 10 seconds max
|
|
931
1053
|
let networkReady = false;
|
|
932
1054
|
for (let probeNum = 1; probeNum <= maxProbeAttempts; probeNum++) {
|
|
1055
|
+
// Declared outside the try so the `finally` can always clear it — the fetch
|
|
1056
|
+
// rejects on most early probes, and an un-cleared BgTimer is a live native
|
|
1057
|
+
// timer (up to 20 per attempt, 100 per sync).
|
|
1058
|
+
const probeController = new AbortController();
|
|
1059
|
+
let probeTimeout = null;
|
|
933
1060
|
try {
|
|
934
1061
|
console.log(`[GallerySyncService] 🍎 Connectivity probe ${probeNum}/${maxProbeAttempts}...`);
|
|
935
1062
|
// Try to reach the glasses health endpoint with a short timeout
|
|
936
|
-
|
|
937
|
-
const probeTimeout = BgTimer.setTimeout(() => probeController.abort(), 1000); // 1 second timeout per probe
|
|
1063
|
+
probeTimeout = BgTimer.setTimeout(() => probeController.abort(), 1000); // 1 second timeout per probe
|
|
938
1064
|
const probeStartTime = Date.now();
|
|
939
1065
|
const probeResponse = await localNetworkTransport.fetch(`http://${hotspotInfo.ip}:8089/api/health`, {
|
|
940
1066
|
method: "GET",
|
|
941
1067
|
signal: probeController.signal,
|
|
942
1068
|
});
|
|
943
|
-
BgTimer.clearTimeout(probeTimeout);
|
|
944
1069
|
const probeDuration = Date.now() - probeStartTime;
|
|
945
1070
|
console.log(`[GallerySyncService] 🍎 Probe ${probeNum} response: HTTP ${probeResponse.status} (${probeDuration}ms)`);
|
|
946
1071
|
if (probeResponse.status === 200 || probeResponse.status === 404) {
|
|
@@ -952,10 +1077,14 @@ class GallerySyncService {
|
|
|
952
1077
|
}
|
|
953
1078
|
}
|
|
954
1079
|
catch (probeError) {
|
|
955
|
-
const errorMsg = probeError
|
|
1080
|
+
const errorMsg = probeError instanceof Error ? probeError.message : String(probeError);
|
|
956
1081
|
console.log(`[GallerySyncService] 🍎 Probe ${probeNum} failed: ${errorMsg.substring(0, 50)}${errorMsg.length > 50 ? "..." : ""}`);
|
|
957
1082
|
// Continue to next probe
|
|
958
1083
|
}
|
|
1084
|
+
finally {
|
|
1085
|
+
if (probeTimeout !== null)
|
|
1086
|
+
BgTimer.clearTimeout(probeTimeout);
|
|
1087
|
+
}
|
|
959
1088
|
// Wait 500ms before next probe (unless this was the last attempt)
|
|
960
1089
|
if (probeNum < maxProbeAttempts) {
|
|
961
1090
|
await new Promise((resolve) => BgTimer.setTimeout(() => resolve(), 500));
|
|
@@ -1103,6 +1232,13 @@ class GallerySyncService {
|
|
|
1103
1232
|
userErrorMessage =
|
|
1104
1233
|
"Could not connect to glasses WiFi. Please ensure you accept the WiFi prompt when it appears.";
|
|
1105
1234
|
}
|
|
1235
|
+
else if (!this.ssidReadable) {
|
|
1236
|
+
// We fell back to the connectivity probe because Location was denied, and the probe
|
|
1237
|
+
// did not reach the glasses either. Point the user at the actionable cause rather
|
|
1238
|
+
// than a raw "could not reach 192.168.43.1:8089".
|
|
1239
|
+
userErrorMessage = "Could not reach glasses over WiFi. Allow Location access so the app can verify the network.";
|
|
1240
|
+
emitGalleryNotice({ code: "location_permission_required" });
|
|
1241
|
+
}
|
|
1106
1242
|
store.setSyncError(userErrorMessage);
|
|
1107
1243
|
if (store.syncServiceOpenedHotspot) {
|
|
1108
1244
|
await this.closeHotspot();
|