From 5546e933ebffd80f9ee7f8e8564497fdf39a4390 Mon Sep 17 00:00:00 2001 From: justanotheratom Date: Wed, 26 Nov 2025 08:32:57 +0530 Subject: [PATCH 1/6] Add ping endpoint and latency tracking to PostHog - Add PING endpoint to SafeEatsEndpoint enum - Create NetworkInfo utility for network type, cellular generation, and carrier detection - Create PingRepository for measuring backend latency - Integrate ping call after successful login (once per app lifecycle) - Add trackEdgePing() to Analytics for PostHog logging - Add comprehensive logging throughout ping flow - Log latency and metadata (app_version, device_model, network_type, cellular_generation, carrier) to PostHog The ping endpoint is called non-blocking after device registration completes. All metadata is collected and logged to PostHog with event name 'edge_ping'. --- .../lc/fungee/IngrediCheck/MainActivity.kt | 7 +- .../IngrediCheck/analytics/Analytics.kt | 10 ++ .../model/entities/SafeEatsEndpoint.kt | 3 +- .../model/repository/PingRepository.kt | 67 +++++++++ .../IngrediCheck/model/utils/Constants.kt | 1 + .../IngrediCheck/model/utils/NetworkInfo.kt | 138 ++++++++++++++++++ .../viewmodel/LoginAuthViewModel.kt | 114 ++++++++++++++- .../viewmodel/LoginAuthViewModelFactory.kt | 6 +- 8 files changed, 334 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/lc/fungee/IngrediCheck/model/repository/PingRepository.kt create mode 100644 app/src/main/java/lc/fungee/IngrediCheck/model/utils/NetworkInfo.kt diff --git a/app/src/main/java/lc/fungee/IngrediCheck/MainActivity.kt b/app/src/main/java/lc/fungee/IngrediCheck/MainActivity.kt index a50d0c0..1c9fc6f 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/MainActivity.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/MainActivity.kt @@ -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 @@ -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) diff --git a/app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt b/app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt index ff23f83..07644db 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt @@ -168,4 +168,14 @@ object Analytics { fun reset() { PostHog.reset() } + + // Edge ping latency tracking + fun trackEdgePing(properties: Map) { + 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'") + } } diff --git a/app/src/main/java/lc/fungee/IngrediCheck/model/entities/SafeEatsEndpoint.kt b/app/src/main/java/lc/fungee/IngrediCheck/model/entities/SafeEatsEndpoint.kt index 28b8dda..96886bf 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/model/entities/SafeEatsEndpoint.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/model/entities/SafeEatsEndpoint.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/lc/fungee/IngrediCheck/model/repository/PingRepository.kt b/app/src/main/java/lc/fungee/IngrediCheck/model/repository/PingRepository.kt new file mode 100644 index 0000000..89789c4 --- /dev/null +++ b/app/src/main/java/lc/fungee/IngrediCheck/model/repository/PingRepository.kt @@ -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 + } + } +} + diff --git a/app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt b/app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt index c6494ec..e5bd998 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt @@ -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 { diff --git a/app/src/main/java/lc/fungee/IngrediCheck/model/utils/NetworkInfo.kt b/app/src/main/java/lc/fungee/IngrediCheck/model/utils/NetworkInfo.kt new file mode 100644 index 0000000..1bbd96a --- /dev/null +++ b/app/src/main/java/lc/fungee/IngrediCheck/model/utils/NetworkInfo.kt @@ -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 + } + } +} + diff --git a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt index 14d11f9..bd6767d 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt @@ -19,8 +19,11 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import lc.fungee.IngrediCheck.model.repository.DeviceRepository import lc.fungee.IngrediCheck.model.repository.LoginAuthRepository +import lc.fungee.IngrediCheck.model.repository.PingRepository import lc.fungee.IngrediCheck.analytics.Analytics import lc.fungee.IngrediCheck.IngrediCheckApp +import lc.fungee.IngrediCheck.model.utils.NetworkInfo +import android.content.pm.PackageManager sealed class AppleLoginState { object Idle : AppleLoginState() @@ -32,7 +35,8 @@ sealed class AppleLoginState { class AppleAuthViewModel( private val repository: LoginAuthRepository, - private val deviceRepository: DeviceRepository + private val deviceRepository: DeviceRepository, + private val pingRepository: PingRepository ) : ViewModel() { var userEmail by mutableStateOf(null) private set @@ -378,10 +382,12 @@ class AppleAuthViewModel( userEmail = null userId = null _loginState.value = AppleLoginState.Idle - // Clear login provider - context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) - .edit() + // Clear login provider but preserve ping flag (once per app lifecycle) + val prefs = context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) + val pingCalled = prefs.getBoolean(AppConstants.Prefs.KEY_PING_CALLED, false) + prefs.edit() .clear() + .putBoolean(AppConstants.Prefs.KEY_PING_CALLED, pingCalled) .apply() serverInternalMode = false deviceRegistrationCompleted = false @@ -394,9 +400,11 @@ class AppleAuthViewModel( userEmail = null userId = null _loginState.value = AppleLoginState.Idle - context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) - .edit() + val prefs = context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) + val pingCalled = prefs.getBoolean(AppConstants.Prefs.KEY_PING_CALLED, false) + prefs.edit() .clear() + .putBoolean(AppConstants.Prefs.KEY_PING_CALLED, pingCalled) .apply() } ) @@ -406,9 +414,11 @@ class AppleAuthViewModel( userEmail = null userId = null _loginState.value = AppleLoginState.Idle - context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) - .edit() + val prefs = context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) + val pingCalled = prefs.getBoolean(AppConstants.Prefs.KEY_PING_CALLED, false) + prefs.edit() .clear() + .putBoolean(AppConstants.Prefs.KEY_PING_CALLED, pingCalled) .apply() serverInternalMode = false deviceRegistrationCompleted = false @@ -456,6 +466,8 @@ class AppleAuthViewModel( val isInternal = deviceRepository.registerDevice(deviceId, markInternal) deviceRegistrationCompleted = true setInternalUser(isInternal) + // Call ping after successful device registration + callPingOnce(ctx, session) }.onFailure { Log.e("AppleAuthViewModel", "Failed to register device", it) }.also { @@ -464,6 +476,92 @@ class AppleAuthViewModel( } } + private fun callPingOnce(context: Context, session: UserSession) { + Log.d("AppleAuthViewModel", "callPingOnce() called") + val prefs = context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) + val pingCalled = prefs.getBoolean(AppConstants.Prefs.KEY_PING_CALLED, false) + + if (pingCalled) { + Log.d("AppleAuthViewModel", "Ping already called in this app lifecycle, skipping") + return + } + + Log.d("AppleAuthViewModel", "Ping not called yet, proceeding with ping call") + viewModelScope.launch { + try { + Log.d("AppleAuthViewModel", "Starting ping call in coroutine") + val token = session.accessToken + Log.d("AppleAuthViewModel", "Got access token, length: ${token.length}") + + Log.d("AppleAuthViewModel", "Calling pingRepository.ping()...") + val latencyMs = pingRepository.ping(token) + + if (latencyMs != null) { + Log.i("AppleAuthViewModel", "✓ Ping successful! Latency: ${latencyMs}ms") + + // Collect network metadata and app info + Log.d("AppleAuthViewModel", "Collecting metadata...") + val appVersion = try { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + val version = packageInfo.versionName ?: "unknown" + Log.d("AppleAuthViewModel", "App version: $version") + version + } catch (e: Exception) { + Log.w("AppleAuthViewModel", "Failed to get app version: ${e.message}") + "unknown" + } + + val deviceModel = Build.MODEL + Log.d("AppleAuthViewModel", "Device model: $deviceModel") + + Log.d("AppleAuthViewModel", "Getting network type...") + val networkType = NetworkInfo.getNetworkType(context) + Log.d("AppleAuthViewModel", "Network type: $networkType") + + Log.d("AppleAuthViewModel", "Getting cellular generation...") + val cellularGeneration = NetworkInfo.getCellularGeneration(context) + Log.d("AppleAuthViewModel", "Cellular generation: $cellularGeneration") + + Log.d("AppleAuthViewModel", "Getting carrier...") + val carrier = NetworkInfo.getCarrier(context) + Log.d("AppleAuthViewModel", "Carrier: ${carrier ?: "null"}") + + val properties = mutableMapOf( + "client_latency_ms" to latencyMs, + "app_version" to appVersion, + "device_model" to deviceModel, + "network_type" to networkType, + "cellular_generation" to cellularGeneration + ) + + if (!carrier.isNullOrBlank()) { + properties["carrier"] = carrier + Log.d("AppleAuthViewModel", "Added carrier to properties: $carrier") + } + + Log.i("AppleAuthViewModel", "edge_ping properties collected:") + properties.forEach { (key, value) -> + Log.i("AppleAuthViewModel", " $key = $value") + } + + Log.d("AppleAuthViewModel", "Calling Analytics.trackEdgePing()...") + Analytics.trackEdgePing(properties) + Log.i("AppleAuthViewModel", "✓ PostHog event 'edge_ping' sent successfully!") + + // Mark ping as called + prefs.edit().putBoolean(AppConstants.Prefs.KEY_PING_CALLED, true).apply() + Log.d("AppleAuthViewModel", "Marked ping as called in SharedPreferences") + } else { + Log.w("AppleAuthViewModel", "✗ Ping failed, not logging to PostHog") + } + } catch (e: Exception) { + Log.e("AppleAuthViewModel", "✗ Exception in callPingOnce", e) + Log.e("AppleAuthViewModel", "Exception type: ${e.javaClass.simpleName}, message: ${e.message}") + e.printStackTrace() + } + } + } + private fun isDebugBuildOrEmulator(context: Context): Boolean { val isDebuggable = (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 if (isDebuggable) return true diff --git a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModelFactory.kt b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModelFactory.kt index 68e1121..9de9a4e 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModelFactory.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModelFactory.kt @@ -4,15 +4,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import lc.fungee.IngrediCheck.model.repository.DeviceRepository import lc.fungee.IngrediCheck.model.repository.LoginAuthRepository +import lc.fungee.IngrediCheck.model.repository.PingRepository class LoginAuthViewModelFactory( private val repository: LoginAuthRepository, - private val deviceRepository: DeviceRepository + private val deviceRepository: DeviceRepository, + private val pingRepository: PingRepository ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(AppleAuthViewModel::class.java)) { - return AppleAuthViewModel(repository, deviceRepository) as T + return AppleAuthViewModel(repository, deviceRepository, pingRepository) as T } throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } From 0161daaec7e270776bacc8a8340204c66a906162 Mon Sep 17 00:00:00 2001 From: justanotheratom Date: Wed, 26 Nov 2025 11:10:07 +0530 Subject: [PATCH 2/6] Remove verbose debug logs from ping implementation - Remove detailed debug logging from NetworkInfo, PingRepository, LoginAuthViewModel, and Analytics - Keep essential error logging for debugging failures - Clean up code for production readiness --- .../IngrediCheck/analytics/Analytics.kt | 5 -- .../model/repository/PingRepository.kt | 17 +----- .../IngrediCheck/model/utils/NetworkInfo.kt | 55 ++++--------------- .../viewmodel/LoginAuthViewModel.kt | 41 +------------- 4 files changed, 15 insertions(+), 103 deletions(-) diff --git a/app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt b/app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt index 07644db..fe2bcd6 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt @@ -171,11 +171,6 @@ object Analytics { // Edge ping latency tracking fun trackEdgePing(properties: Map) { - 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'") } } diff --git a/app/src/main/java/lc/fungee/IngrediCheck/model/repository/PingRepository.kt b/app/src/main/java/lc/fungee/IngrediCheck/model/repository/PingRepository.kt index 89789c4..94a3abf 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/model/repository/PingRepository.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/model/repository/PingRepository.kt @@ -26,9 +26,6 @@ class PingRepository( 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() @@ -37,29 +34,19 @@ class PingRepository( .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") + Log.w("PingRepository", "Ping failed with status ${response.code}") 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}") + Log.e("PingRepository", "Ping API call failed", e) null } } diff --git a/app/src/main/java/lc/fungee/IngrediCheck/model/utils/NetworkInfo.kt b/app/src/main/java/lc/fungee/IngrediCheck/model/utils/NetworkInfo.kt index 1bbd96a..6401ca3 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/model/utils/NetworkInfo.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/model/utils/NetworkInfo.kt @@ -5,7 +5,6 @@ import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.os.Build import android.telephony.TelephonyManager -import android.util.Log object NetworkInfo { /** @@ -15,26 +14,15 @@ object NetworkInfo { 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 network = connectivityManager.activeNetwork ?: return "none" + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return "none" - val networkType = when { + return 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 } /** @@ -42,23 +30,15 @@ object NetworkInfo { */ 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" - } + if (networkType != "cellular") return "none" val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager - if (telephonyManager == null) { - Log.w("NetworkInfo", "TelephonyManager not available, returning 'unknown'") - return "unknown" - } + ?: return "unknown" - val generation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return 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) { + when (telephonyManager.dataNetworkType) { TelephonyManager.NETWORK_TYPE_GPRS, TelephonyManager.NETWORK_TYPE_EDGE, TelephonyManager.NETWORK_TYPE_CDMA, @@ -80,9 +60,7 @@ object NetworkInfo { } else { // Android 10 and below @Suppress("DEPRECATION") - val networkTypeValue = telephonyManager.networkType - Log.d("NetworkInfo", "Android 10-, networkType: $networkTypeValue") - when (networkTypeValue) { + when (telephonyManager.networkType) { TelephonyManager.NETWORK_TYPE_GPRS, TelephonyManager.NETWORK_TYPE_EDGE, TelephonyManager.NETWORK_TYPE_CDMA, @@ -102,8 +80,6 @@ object NetworkInfo { else -> "unknown" } } - Log.d("NetworkInfo", "Detected cellular generation: $generation") - return generation } /** @@ -111,26 +87,17 @@ object NetworkInfo { */ 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 - } + if (networkType != "cellular") 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 null return try { @Suppress("DEPRECATION") - val carrierName = telephonyManager.networkOperatorName?.takeIf { it.isNotBlank() } - Log.d("NetworkInfo", "Detected carrier: ${carrierName ?: "null/empty"}") - carrierName + telephonyManager.networkOperatorName?.takeIf { it.isNotBlank() } } catch (e: SecurityException) { // READ_PHONE_STATE permission may not be granted - Log.w("NetworkInfo", "SecurityException getting carrier (permission denied): ${e.message}") null } } diff --git a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt index bd6767d..40db4f1 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt @@ -477,54 +477,30 @@ class AppleAuthViewModel( } private fun callPingOnce(context: Context, session: UserSession) { - Log.d("AppleAuthViewModel", "callPingOnce() called") val prefs = context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) val pingCalled = prefs.getBoolean(AppConstants.Prefs.KEY_PING_CALLED, false) if (pingCalled) { - Log.d("AppleAuthViewModel", "Ping already called in this app lifecycle, skipping") return } - Log.d("AppleAuthViewModel", "Ping not called yet, proceeding with ping call") viewModelScope.launch { try { - Log.d("AppleAuthViewModel", "Starting ping call in coroutine") val token = session.accessToken - Log.d("AppleAuthViewModel", "Got access token, length: ${token.length}") - - Log.d("AppleAuthViewModel", "Calling pingRepository.ping()...") val latencyMs = pingRepository.ping(token) if (latencyMs != null) { - Log.i("AppleAuthViewModel", "✓ Ping successful! Latency: ${latencyMs}ms") - // Collect network metadata and app info - Log.d("AppleAuthViewModel", "Collecting metadata...") val appVersion = try { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) - val version = packageInfo.versionName ?: "unknown" - Log.d("AppleAuthViewModel", "App version: $version") - version + packageInfo.versionName ?: "unknown" } catch (e: Exception) { - Log.w("AppleAuthViewModel", "Failed to get app version: ${e.message}") "unknown" } - val deviceModel = Build.MODEL - Log.d("AppleAuthViewModel", "Device model: $deviceModel") - - Log.d("AppleAuthViewModel", "Getting network type...") val networkType = NetworkInfo.getNetworkType(context) - Log.d("AppleAuthViewModel", "Network type: $networkType") - - Log.d("AppleAuthViewModel", "Getting cellular generation...") val cellularGeneration = NetworkInfo.getCellularGeneration(context) - Log.d("AppleAuthViewModel", "Cellular generation: $cellularGeneration") - - Log.d("AppleAuthViewModel", "Getting carrier...") val carrier = NetworkInfo.getCarrier(context) - Log.d("AppleAuthViewModel", "Carrier: ${carrier ?: "null"}") val properties = mutableMapOf( "client_latency_ms" to latencyMs, @@ -536,28 +512,15 @@ class AppleAuthViewModel( if (!carrier.isNullOrBlank()) { properties["carrier"] = carrier - Log.d("AppleAuthViewModel", "Added carrier to properties: $carrier") } - Log.i("AppleAuthViewModel", "edge_ping properties collected:") - properties.forEach { (key, value) -> - Log.i("AppleAuthViewModel", " $key = $value") - } - - Log.d("AppleAuthViewModel", "Calling Analytics.trackEdgePing()...") Analytics.trackEdgePing(properties) - Log.i("AppleAuthViewModel", "✓ PostHog event 'edge_ping' sent successfully!") // Mark ping as called prefs.edit().putBoolean(AppConstants.Prefs.KEY_PING_CALLED, true).apply() - Log.d("AppleAuthViewModel", "Marked ping as called in SharedPreferences") - } else { - Log.w("AppleAuthViewModel", "✗ Ping failed, not logging to PostHog") } } catch (e: Exception) { - Log.e("AppleAuthViewModel", "✗ Exception in callPingOnce", e) - Log.e("AppleAuthViewModel", "Exception type: ${e.javaClass.simpleName}, message: ${e.message}") - e.printStackTrace() + Log.e("AppleAuthViewModel", "Ping API call failed", e) } } } From 6b902c852592a0dd6d65620f8e65cea1e5316d32 Mon Sep 17 00:00:00 2001 From: justanotheratom Date: Wed, 26 Nov 2025 11:20:50 +0530 Subject: [PATCH 3/6] Fix: Use in-memory flag for ping once-per-lifecycle guard - Replace SharedPreferences persistence with in-memory @Volatile flag - Ping flag now resets on app restart (true once-per-lifecycle behavior) - Remove KEY_PING_CALLED from AppConstants (no longer needed) - Remove ping flag preservation logic from signOut methods Previously, the ping flag persisted across app restarts via SharedPreferences, causing ping to be skipped on every subsequent app launch after the first. Now the flag is stored in-memory only and resets when the app process restarts. --- .../viewmodel/LoginAuthViewModel.kt | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt index 40db4f1..0ad78eb 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt @@ -64,6 +64,8 @@ class AppleAuthViewModel( private var deviceRegistrationCompleted = false @Volatile private var deviceRegistrationInProgress = false + @Volatile + private var pingCalled = false // Observable state for effective internal mode private val _effectiveInternalMode = MutableStateFlow(false) @@ -382,12 +384,10 @@ class AppleAuthViewModel( userEmail = null userId = null _loginState.value = AppleLoginState.Idle - // Clear login provider but preserve ping flag (once per app lifecycle) - val prefs = context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) - val pingCalled = prefs.getBoolean(AppConstants.Prefs.KEY_PING_CALLED, false) - prefs.edit() + // Clear login provider + context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) + .edit() .clear() - .putBoolean(AppConstants.Prefs.KEY_PING_CALLED, pingCalled) .apply() serverInternalMode = false deviceRegistrationCompleted = false @@ -400,11 +400,9 @@ class AppleAuthViewModel( userEmail = null userId = null _loginState.value = AppleLoginState.Idle - val prefs = context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) - val pingCalled = prefs.getBoolean(AppConstants.Prefs.KEY_PING_CALLED, false) - prefs.edit() + context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) + .edit() .clear() - .putBoolean(AppConstants.Prefs.KEY_PING_CALLED, pingCalled) .apply() } ) @@ -477,9 +475,6 @@ class AppleAuthViewModel( } private fun callPingOnce(context: Context, session: UserSession) { - val prefs = context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) - val pingCalled = prefs.getBoolean(AppConstants.Prefs.KEY_PING_CALLED, false) - if (pingCalled) { return } @@ -516,8 +511,8 @@ class AppleAuthViewModel( Analytics.trackEdgePing(properties) - // Mark ping as called - prefs.edit().putBoolean(AppConstants.Prefs.KEY_PING_CALLED, true).apply() + // Mark ping as called (in-memory only, resets on app restart) + pingCalled = true } } catch (e: Exception) { Log.e("AppleAuthViewModel", "Ping API call failed", e) From 14a143b3de185a37f3cf9949aa84e071fdc25ea9 Mon Sep 17 00:00:00 2001 From: justanotheratom Date: Wed, 26 Nov 2025 11:23:42 +0530 Subject: [PATCH 4/6] Remove KEY_PING_CALLED constant (no longer needed) --- .../main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt b/app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt index e5bd998..c6494ec 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt @@ -31,7 +31,6 @@ 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 { From 185ad0d45ad33c6e1b6b90fc6fecd78cf38ca1f9 Mon Sep 17 00:00:00 2001 From: justanotheratom Date: Wed, 26 Nov 2025 11:26:55 +0530 Subject: [PATCH 5/6] Remove remaining KEY_PING_CALLED reference in signOut catch block --- .../lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt index 0ad78eb..a8c4e01 100644 --- a/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt +++ b/app/src/main/java/lc/fungee/IngrediCheck/viewmodel/LoginAuthViewModel.kt @@ -412,11 +412,9 @@ class AppleAuthViewModel( userEmail = null userId = null _loginState.value = AppleLoginState.Idle - val prefs = context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) - val pingCalled = prefs.getBoolean(AppConstants.Prefs.KEY_PING_CALLED, false) - prefs.edit() + context.getSharedPreferences(AppConstants.Prefs.USER_SESSION, Context.MODE_PRIVATE) + .edit() .clear() - .putBoolean(AppConstants.Prefs.KEY_PING_CALLED, pingCalled) .apply() serverInternalMode = false deviceRegistrationCompleted = false From d5fbc0d0fa91bace8996ff32b95850d39c1778e7 Mon Sep 17 00:00:00 2001 From: justanotheratom Date: Wed, 26 Nov 2025 12:28:54 +0530 Subject: [PATCH 6/6] Bump version to 1.2.2 --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c24da5f..6a7eaca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,7 +15,7 @@ android { minSdk = 31 targetSdk = 36 versionCode = 7 - versionName = "1.2.1" + versionName = "1.2.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }