Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public class MediaCaptureService {
// Debug flag: Enable detailed end-to-end photo capture timing logs
// true = log timing from request to capture, false = suppress timing logs
private static final boolean ENABLE_PHOTO_TIMING_LOGS = true;
private static final int DEFAULT_PHOTO_UPLOAD_CONNECT_TIMEOUT_SECONDS = 5;
private static final int LOCAL_PHOTO_UPLOAD_CONNECT_TIMEOUT_SECONDS = 1;

private final Context mContext;
private final MediaUploadQueueManager mMediaQueueManager;
Expand Down Expand Up @@ -2337,13 +2339,28 @@ private void performDirectUpload(
// - 5 seconds to connect (allows time for DNS+TCP+TLS over WiFi)
// - 10 seconds to write the photo data
// - 5 seconds to read the response
int connectTimeoutSeconds =
isLocalNetworkWebhook(webhookUrl)
? LOCAL_PHOTO_UPLOAD_CONNECT_TIMEOUT_SECONDS
: DEFAULT_PHOTO_UPLOAD_CONNECT_TIMEOUT_SECONDS;
if (connectTimeoutSeconds
< DEFAULT_PHOTO_UPLOAD_CONNECT_TIMEOUT_SECONDS) {
Log.d(
TAG,
"⚡ Using fast local receiver connect timeout ("
+ connectTimeoutSeconds
+ "s) for webhook: "
+ webhookUrl);
}

OkHttpClient client =
new OkHttpClient.Builder()
.connectTimeout(
5,
connectTimeoutSeconds,
java.util.concurrent.TimeUnit
.SECONDS) // Allow time for
// DNS+TCP+TLS on WiFi
.SECONDS) // Local receiver
// failures should fall back quickly; public
// webhooks keep the normal WiFi allowance.
.writeTimeout(
10,
java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -2686,6 +2703,41 @@ private void performDirectUpload(
.start();
}

private boolean isLocalNetworkWebhook(String webhookUrl) {
try {
URI uri = URI.create(webhookUrl);
String host = uri.getHost();
if (host == null || host.isEmpty()) {
return false;
}
return isLocalIpv4Literal(host) || "localhost".equalsIgnoreCase(host);
} catch (Exception ignored) {
return false;
}
}

private boolean isLocalIpv4Literal(String host) {
String[] parts = host.split("\\.");
if (parts.length != 4) {
return false;
}
int[] octets = new int[4];
for (int i = 0; i < parts.length; i++) {
try {
octets[i] = Integer.parseInt(parts[i]);
} catch (NumberFormatException e) {
return false;
}
if (octets[i] < 0 || octets[i] > 255) {
return false;
}
}
return octets[0] == 10
|| (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31)
|| (octets[0] == 192 && octets[1] == 168)
|| octets[0] == 127;
}

/** Upload a video file to AugmentOS Cloud Currently a stub - videos are kept on device */
public void uploadVideo(String videoFilePath, String requestId) {
Log.d(TAG, "Video upload not implemented yet. Video saved locally: " + videoFilePath);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.mentra.bluetoothsdk.photoreceiver

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.util.Log
import com.mentra.bluetoothsdk.DeviceStore
import com.mentra.bluetoothsdk.debug.BleTraceLogger
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.modules.Module
Expand Down Expand Up @@ -129,6 +132,10 @@ class MentraPhotoReceiverModule : Module() {
}

private fun bestLocalIpv4Address(): String? {
val glassesLocalIp = (DeviceStore.get("glasses", "wifiLocalIp") as? String)
?.trim()
?.takeIf { it.isNotEmpty() }
val activeNetworkAddress = activeNetworkIpv4Address()
val wifiPrivateCandidates = mutableListOf<Inet4Address>()
val wifiCandidates = mutableListOf<Inet4Address>()
val privateCandidates = mutableListOf<Inet4Address>()
Expand All @@ -153,6 +160,30 @@ class MentraPhotoReceiverModule : Module() {
}
}

if (activeNetworkAddress != null) {
val activeHost = activeNetworkAddress.hostAddress
if (sameIpv4Slash24(activeHost, glassesLocalIp)) {
emitStatus("Using active phone Wi-Fi address $activeHost for glasses $glassesLocalIp")
return activeHost
}
}

val sameSubnetCandidate = (
wifiPrivateCandidates +
wifiCandidates +
privateCandidates +
otherCandidates
).firstOrNull { sameIpv4Slash24(it.hostAddress, glassesLocalIp) }
if (sameSubnetCandidate != null) {
emitStatus("Using same-subnet phone address ${sameSubnetCandidate.hostAddress} for glasses $glassesLocalIp")
return sameSubnetCandidate.hostAddress
}

if (activeNetworkAddress != null && isPrivateIpv4(activeNetworkAddress.hostAddress.orEmpty())) {
emitStatus("Using active phone network address ${activeNetworkAddress.hostAddress}")
return activeNetworkAddress.hostAddress
}

return (
wifiPrivateCandidates.firstOrNull() ?:
wifiCandidates.firstOrNull() ?:
Expand All @@ -161,6 +192,25 @@ class MentraPhotoReceiverModule : Module() {
)?.hostAddress
}

private fun activeNetworkIpv4Address(): Inet4Address? {
val connectivityManager = reactContext()
.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return null
val activeNetwork = connectivityManager.activeNetwork ?: return null
val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return null
val isLanNetwork = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
if (!isLanNetwork) return null
val linkProperties = connectivityManager.getLinkProperties(activeNetwork) ?: return null
return linkProperties.linkAddresses
.map { it.address }
.filterIsInstance<Inet4Address>()
.firstOrNull { address ->
!address.isLoopbackAddress &&
!address.isLinkLocalAddress &&
isPrivateIpv4(address.hostAddress.orEmpty())
}
}

private fun receiverEndpoint(host: String, port: Int): ReceiverEndpoint {
return ReceiverEndpoint(
uploadUrl = "http://$host:$port/upload",
Expand Down Expand Up @@ -190,6 +240,16 @@ class MentraPhotoReceiverModule : Module() {
host.matches(Regex("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"))
}

private fun sameIpv4Slash24(first: String?, second: String?): Boolean {
if (first.isNullOrBlank() || second.isNullOrBlank()) return false
val firstParts = first.split(".")
val secondParts = second.split(".")
if (firstParts.size != 4 || secondParts.size != 4) return false
return firstParts[0] == secondParts[0] &&
firstParts[1] == secondParts[1] &&
firstParts[2] == secondParts[2]
}

private companion object {
const val TAG = "MentraPhotoReceiver"
val PHOTO_PORTS = listOf(8787, 8788, 8789, 8790)
Expand Down
Loading