Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion app/src/main/java/lc/fungee/IngrediCheck/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import lc.fungee.IngrediCheck.ui.theme.IngrediCheckTheme
import lc.fungee.IngrediCheck.model.repository.DeviceRepository
import lc.fungee.IngrediCheck.model.repository.LoginAuthRepository
import lc.fungee.IngrediCheck.model.repository.PreferenceRepository
import lc.fungee.IngrediCheck.model.repository.PingRepository
import lc.fungee.IngrediCheck.viewmodel.AppleAuthViewModel
import lc.fungee.IngrediCheck.viewmodel.AppleLoginState
import lc.fungee.IngrediCheck.viewmodel.LoginAuthViewModelFactory
Expand Down Expand Up @@ -77,7 +78,11 @@ class MainActivity : ComponentActivity() {
supabaseClient = repository.supabaseClient,
functionsBaseUrl = AppConstants.Functions.base
)
val vmFactory = LoginAuthViewModelFactory(repository, deviceRepository)
val pingRepository = PingRepository(
functionsBaseUrl = AppConstants.Functions.base,
anonKey = supabaseAnonKey
)
val vmFactory = LoginAuthViewModelFactory(repository, deviceRepository, pingRepository)
authViewModel = ViewModelProvider(this, vmFactory)
.get(AppleAuthViewModel::class.java)

Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,14 @@ object Analytics {
fun reset() {
PostHog.reset()
}

// Edge ping latency tracking
fun trackEdgePing(properties: Map<String, Any>) {
android.util.Log.d("Analytics", "trackEdgePing() called with properties:")
properties.forEach { (key, value) ->
android.util.Log.d("Analytics", " $key = $value")
}
capture(event = "edge_ping", properties = properties)
android.util.Log.i("Analytics", "✓ PostHog.capture() called for event 'edge_ping'")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ enum class SafeEatsEndpoint(private val pathFormat: String) {
PREFERENCE_LISTS_DEFAULT_ITEMS("preferencelists/default/%s"),
DEVICES_REGISTER("devices/register"),
DEVICES_MARK_INTERNAL("devices/mark-internal"),
DEVICES_IS_INTERNAL("devices/%s/is-internal");
DEVICES_IS_INTERNAL("devices/%s/is-internal"),
PING("ping");

fun format(vararg args: String): String = if (args.isEmpty()) pathFormat else String.format(pathFormat, *args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package lc.fungee.IngrediCheck.model.repository

import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import lc.fungee.IngrediCheck.model.entities.SafeEatsEndpoint
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.concurrent.TimeUnit

/**
* Repository for ping endpoint to measure backend latency
*/
class PingRepository(
private val functionsBaseUrl: String,
private val anonKey: String,
private val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.callTimeout(10, TimeUnit.SECONDS)
.build()
) {
/**
* Ping the backend and return latency in milliseconds, or null on failure
*/
suspend fun ping(token: String): Long? = withContext(Dispatchers.IO) {
try {
val url = "$functionsBaseUrl/${SafeEatsEndpoint.PING.format()}"
Log.d("PingRepository", "Starting ping to: $url")
Log.d("PingRepository", "Token length: ${token.length}, Token preview: ${token.take(20)}...")

val request = Request.Builder()
.url(url)
.get()
.addHeader("Authorization", "Bearer $token")
.addHeader("apikey", anonKey)
.build()

val startTime = System.currentTimeMillis()
Log.d("PingRepository", "Ping request started at: $startTime ms")

client.newCall(request).execute().use { response ->
val endTime = System.currentTimeMillis()
val latencyMs = endTime - startTime

Log.d("PingRepository", "Ping response received at: $endTime ms")
Log.d("PingRepository", "Ping response code=${response.code}, latency=${latencyMs}ms")
Log.d("PingRepository", "Response headers: ${response.headers}")

if (response.code == 204) {
Log.i("PingRepository", "✓ Ping successful! Latency: ${latencyMs}ms")
latencyMs
} else {
Log.w("PingRepository", "✗ Ping failed with status ${response.code}")
val responseBody = response.body?.string()?.take(200)
Log.w("PingRepository", "Response body: $responseBody")
null
}
}
} catch (e: Exception) {
Log.e("PingRepository", "✗ Ping API call failed with exception", e)
Log.e("PingRepository", "Exception type: ${e.javaClass.simpleName}, message: ${e.message}")
null
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ object AppConstants {
const val KEY_LOGIN_PROVIDER = "login_provider"
const val KEY_DISCLAIMER_ACCEPTED = "disclaimer_accepted"
const val KEY_DEVICE_ID = "device_id"
const val KEY_PING_CALLED = "ping_called"
}

object Providers {
Expand Down
138 changes: 138 additions & 0 deletions app/src/main/java/lc/fungee/IngrediCheck/model/utils/NetworkInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package lc.fungee.IngrediCheck.model.utils

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.telephony.TelephonyManager
import android.util.Log

object NetworkInfo {
/**
* Get network type: "wifi", "cellular", "other", or "none"
*/
fun getNetworkType(context: Context): String {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

val network = connectivityManager.activeNetwork
if (network == null) {
Log.d("NetworkInfo", "No active network found, returning 'none'")
return "none"
}

val capabilities = connectivityManager.getNetworkCapabilities(network)
if (capabilities == null) {
Log.d("NetworkInfo", "No network capabilities found, returning 'none'")
return "none"
}

val networkType = when {
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "other"
else -> "none"
}
Log.d("NetworkInfo", "Detected network type: $networkType")
return networkType
}

/**
* Get cellular generation: "3g", "4g", "5g", "unknown", or "none"
*/
fun getCellularGeneration(context: Context): String {
val networkType = getNetworkType(context)
if (networkType != "cellular") {
Log.d("NetworkInfo", "Not on cellular network, returning 'none' for cellular generation")
return "none"
}

val telephonyManager =
context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
if (telephonyManager == null) {
Log.w("NetworkInfo", "TelephonyManager not available, returning 'unknown'")
return "unknown"
}

val generation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+ (API 30+)
val networkTypeValue = telephonyManager.dataNetworkType
Log.d("NetworkInfo", "Android 11+, dataNetworkType: $networkTypeValue")
when (networkTypeValue) {
TelephonyManager.NETWORK_TYPE_GPRS,
TelephonyManager.NETWORK_TYPE_EDGE,
TelephonyManager.NETWORK_TYPE_CDMA,
TelephonyManager.NETWORK_TYPE_1xRTT,
TelephonyManager.NETWORK_TYPE_IDEN -> "3g"
TelephonyManager.NETWORK_TYPE_UMTS,
TelephonyManager.NETWORK_TYPE_EVDO_0,
TelephonyManager.NETWORK_TYPE_EVDO_A,
TelephonyManager.NETWORK_TYPE_HSDPA,
TelephonyManager.NETWORK_TYPE_HSUPA,
TelephonyManager.NETWORK_TYPE_HSPA,
TelephonyManager.NETWORK_TYPE_EVDO_B,
TelephonyManager.NETWORK_TYPE_EHRPD,
TelephonyManager.NETWORK_TYPE_HSPAP -> "3g"
TelephonyManager.NETWORK_TYPE_LTE -> "4g"
TelephonyManager.NETWORK_TYPE_NR -> "5g"
else -> "unknown"
}
} else {
// Android 10 and below
@Suppress("DEPRECATION")
val networkTypeValue = telephonyManager.networkType
Log.d("NetworkInfo", "Android 10-, networkType: $networkTypeValue")
when (networkTypeValue) {
TelephonyManager.NETWORK_TYPE_GPRS,
TelephonyManager.NETWORK_TYPE_EDGE,
TelephonyManager.NETWORK_TYPE_CDMA,
TelephonyManager.NETWORK_TYPE_1xRTT,
TelephonyManager.NETWORK_TYPE_IDEN -> "3g"
TelephonyManager.NETWORK_TYPE_UMTS,
TelephonyManager.NETWORK_TYPE_EVDO_0,
TelephonyManager.NETWORK_TYPE_EVDO_A,
TelephonyManager.NETWORK_TYPE_HSDPA,
TelephonyManager.NETWORK_TYPE_HSUPA,
TelephonyManager.NETWORK_TYPE_HSPA,
TelephonyManager.NETWORK_TYPE_EVDO_B,
TelephonyManager.NETWORK_TYPE_EHRPD,
TelephonyManager.NETWORK_TYPE_HSPAP -> "3g"
TelephonyManager.NETWORK_TYPE_LTE -> "4g"
TelephonyManager.NETWORK_TYPE_NR -> "5g"
else -> "unknown"
}
}
Log.d("NetworkInfo", "Detected cellular generation: $generation")
return generation
}

/**
* Get carrier name or null if unavailable
*/
fun getCarrier(context: Context): String? {
val networkType = getNetworkType(context)
if (networkType != "cellular") {
Log.d("NetworkInfo", "Not on cellular network, returning null for carrier")
return null
}

val telephonyManager =
context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
if (telephonyManager == null) {
Log.w("NetworkInfo", "TelephonyManager not available, returning null for carrier")
return null
}

return try {
@Suppress("DEPRECATION")
val carrierName = telephonyManager.networkOperatorName?.takeIf { it.isNotBlank() }
Log.d("NetworkInfo", "Detected carrier: ${carrierName ?: "null/empty"}")
carrierName
} catch (e: SecurityException) {
// READ_PHONE_STATE permission may not be granted
Log.w("NetworkInfo", "SecurityException getting carrier (permission denied): ${e.message}")
null
}
}
}

Loading