From 8a577a3fcff9d738e67a92b2abe66b7f66815064 Mon Sep 17 00:00:00 2001 From: Prajjaval Verma Date: Sat, 18 Jan 2025 13:27:20 +0530 Subject: [PATCH] NTP-28744 | Adding NetworkCheck to Timeout errors (#14567) --- .../naviapp/app/ApplicationStateManager.kt | 68 +++++++++ .../initializers/NetworkStatsInitializer.kt | 34 +++++ .../java/com/naviapp/common/di/AppModule.kt | 3 + .../ap/network/retrofit/ApResponseCallback.kt | 8 + .../kotlin/com/navi/ap/network/utils/Utils.kt | 1 + .../common/checkmate/core/CheckMateManager.kt | 6 + .../common/checkmate/utils/CheckMateExt.kt | 5 + .../FirebaseRemoteConfigHelper.kt | 4 + .../com/navi/common/network/ApiConstants.kt | 3 +- .../com/navi/common/network/NetworkUtil.kt | 19 ++- .../resourcemanager/ResourceManagerUseCase.kt | 6 +- .../java/com/navi/common/utils/CommonUtils.kt | 142 +++++++++++++++++- .../java/com/navi/common/utils/Constants.kt | 7 + .../com/navi/common/utils/GenericErrorData.kt | 72 ++++++--- .../com/navi/common/utils/NetworkStats.kt | 114 ++++++++++++++ .../java/com/navi/common/utils/Utility.kt | 16 +- .../java/com/navi/common/viewmodel/BaseVM.kt | 19 ++- .../main/res/xml/default_remote_config.xml | 16 ++ .../health/fragment/ErrorFragment.kt | 2 + .../java/com/navi/insurance/util/Utility.kt | 2 + 20 files changed, 510 insertions(+), 37 deletions(-) create mode 100644 android/app/src/main/java/com/naviapp/app/ApplicationStateManager.kt create mode 100644 android/app/src/main/java/com/naviapp/app/initializers/NetworkStatsInitializer.kt create mode 100644 android/navi-common/src/main/java/com/navi/common/utils/NetworkStats.kt diff --git a/android/app/src/main/java/com/naviapp/app/ApplicationStateManager.kt b/android/app/src/main/java/com/naviapp/app/ApplicationStateManager.kt new file mode 100644 index 0000000000..2d0232d7dc --- /dev/null +++ b/android/app/src/main/java/com/naviapp/app/ApplicationStateManager.kt @@ -0,0 +1,68 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.app + +import android.app.Activity +import androidx.annotation.CallSuper +import com.navi.common.NaviActivityLifecycleCallbacks + +/** + * A base class to manage and monitor the lifecycle of activities in the application and determine + * whether the app is in the foreground (at least one activity is visible to the user) or in the + * background (all activities are stopped). + * + * Key Features: + * - Keeps track of the number of active (started) activities using `foregroundActivitiesCount`. + * - Detects transitions between foreground and background states and invokes appropriate abstract + * methods (`onForeground()` and `onBackground()`) for subclasses to implement. + * + * Usage: + * - Subclasses should implement the `onForeground()` and `onBackground()` methods to define custom + * behavior when the application enters the foreground or background, respectively. + * - Call `initStateManager()` to register lifecycle callbacks. + * + * Note: + * - This class uses `NaviActivityLifecycleCallbacks`, which is a custom implementation of + * `Application.ActivityLifecycleCallbacks`. + */ +abstract class ApplicationStateManager { + + private var isInitialized = false + private var foregroundActivitiesCount = 0 + + @CallSuper + fun initStateManager(application: NaviApplication) { + application.registerActivityLifecycleCallbacks(activityLifecycleCallback) + } + + val activityLifecycleCallback = + object : NaviActivityLifecycleCallbacks() { + + override fun onActivityStarted(activity: Activity) { + foregroundActivitiesCount++ + + if (!isInitialized) { + isInitialized = true + onForeground() + } + } + + override fun onActivityStopped(activity: Activity) { + foregroundActivitiesCount-- + + if (foregroundActivitiesCount == 0) { + isInitialized = false + onBackground() + } + } + } + + abstract fun onForeground() + + abstract fun onBackground() +} diff --git a/android/app/src/main/java/com/naviapp/app/initializers/NetworkStatsInitializer.kt b/android/app/src/main/java/com/naviapp/app/initializers/NetworkStatsInitializer.kt new file mode 100644 index 0000000000..6bd1525253 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/app/initializers/NetworkStatsInitializer.kt @@ -0,0 +1,34 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.app.initializers + +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NETWORK_SPEED_CHECK_FREQUENCY +import com.navi.common.utils.NetworkStats +import com.naviapp.app.ApplicationStateManager +import com.naviapp.app.NaviApplication +import javax.inject.Inject + +class NetworkStatsInitializer @Inject constructor() : + ApplicationStateManager(), ComponentInitializer { + private val networkStatsCheckFrequency: Long by lazy { + FirebaseRemoteConfigHelper.getLong(NETWORK_SPEED_CHECK_FREQUENCY) + } + + override fun initialize(application: NaviApplication) { + super.initStateManager(application) + } + + override fun onForeground() { + NetworkStats.startTracking(updateIntervalMs = networkStatsCheckFrequency) + } + + override fun onBackground() { + NetworkStats.stopTracking() + } +} diff --git a/android/app/src/main/java/com/naviapp/common/di/AppModule.kt b/android/app/src/main/java/com/naviapp/common/di/AppModule.kt index b7a4849700..c50aa038cb 100644 --- a/android/app/src/main/java/com/naviapp/common/di/AppModule.kt +++ b/android/app/src/main/java/com/naviapp/common/di/AppModule.kt @@ -15,6 +15,7 @@ import com.naviapp.app.initializers.AppLifecycleManagerInitializer import com.naviapp.app.initializers.ComponentInitializer import com.naviapp.app.initializers.CrashHandlerInitializer import com.naviapp.app.initializers.NetworkConfigurationInitializer +import com.naviapp.app.initializers.NetworkStatsInitializer import com.naviapp.app.initializers.SdkInitializer import com.naviapp.app.initializers.SignalManagerInitializer import com.naviapp.network.di.CoroutineScopeIO @@ -52,6 +53,7 @@ object AppModule { appLifecycleManagerInitializer: AppLifecycleManagerInitializer, networkConfigurationInitializer: NetworkConfigurationInitializer, signalManagerInitializer: SignalManagerInitializer, + networkStatsInitializer: NetworkStatsInitializer, ): List { return listOf( sdkInitializer, @@ -60,6 +62,7 @@ object AppModule { appLifecycleManagerInitializer, networkConfigurationInitializer, signalManagerInitializer, + networkStatsInitializer, ) } } diff --git a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/network/retrofit/ApResponseCallback.kt b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/network/retrofit/ApResponseCallback.kt index eb1a08f024..e534283292 100644 --- a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/network/retrofit/ApResponseCallback.kt +++ b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/network/retrofit/ApResponseCallback.kt @@ -195,6 +195,14 @@ abstract class ApResponseCallback { description = getString(R.string.no_internet_connection_description) } } + ApiConstants.API_CODE_SLOW_NETWORK -> { + statusCode = ApiConstants.API_CODE_SLOW_NETWORK + methodName = NetworkErrorType.SLOW_INTERNET.name + AppServiceManager.application.apply { + title = getString(R.string.internet_too_slow) + description = getString(R.string.check_internet_connectivity) + } + } ApiConstants.API_CODE_SOCKET_TIMEOUT -> { statusCode = ApiConstants.API_CODE_SOCKET_TIMEOUT methodName = NetworkErrorType.SOCKET_TIMEOUT.name diff --git a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/network/utils/Utils.kt b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/network/utils/Utils.kt index 871eb9269a..394194d0b1 100644 --- a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/network/utils/Utils.kt +++ b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/network/utils/Utils.kt @@ -41,6 +41,7 @@ object NetworkInfoProvider { enum class NetworkErrorType { SOCKET_TIMEOUT, NO_INTERNET, + SLOW_INTERNET, EMPTY_BODY_ERROR, SESSION_EXPIRED, ILLEGAL_ARGUMENT, diff --git a/android/navi-common/src/main/java/com/navi/common/checkmate/core/CheckMateManager.kt b/android/navi-common/src/main/java/com/navi/common/checkmate/core/CheckMateManager.kt index 38012c90c6..d4c49918ca 100644 --- a/android/navi-common/src/main/java/com/navi/common/checkmate/core/CheckMateManager.kt +++ b/android/navi-common/src/main/java/com/navi/common/checkmate/core/CheckMateManager.kt @@ -9,6 +9,7 @@ package com.navi.common.checkmate.core import com.navi.alfred.AlfredManager import com.navi.analytics.utils.NaviTrackEvent +import com.navi.base.AppServiceManager import com.navi.base.utils.orZero import com.navi.common.checkmate.model.EventType import com.navi.common.checkmate.model.MetricInfo @@ -20,6 +21,8 @@ import com.navi.common.checkmate.utils.getEventNameWithVerticalPrefix import com.navi.common.checkmate.utils.getIsNae import com.navi.common.network.models.GenericResponse import com.navi.common.network.models.RepoResult +import com.navi.common.utils.NetworkStats +import com.navi.common.utils.getDownloadNetworkStrength import retrofit2.Response object CheckMateManager { @@ -83,6 +86,9 @@ object CheckMateManager { "alfredSessionId" to AlfredManager.getAlfredSessionId(), "signalLevel" to NaviTrackEvent.signalInfo.level.toString(), "signalType" to NaviTrackEvent.signalInfo.type.name, + "maxBandwidth" to + getDownloadNetworkStrength(AppServiceManager.application).toString(), + "networkSpeed" to NetworkStats.networkSpeed.toString(), "isNae" to getIsNae(isNae = isNae, statusCode = statusCode, exception = exception) .toString(), diff --git a/android/navi-common/src/main/java/com/navi/common/checkmate/utils/CheckMateExt.kt b/android/navi-common/src/main/java/com/navi/common/checkmate/utils/CheckMateExt.kt index b41e97a7aa..49e6d0db8c 100644 --- a/android/navi-common/src/main/java/com/navi/common/checkmate/utils/CheckMateExt.kt +++ b/android/navi-common/src/main/java/com/navi/common/checkmate/utils/CheckMateExt.kt @@ -17,6 +17,7 @@ import com.navi.common.checkmate.model.SessionDetails import com.navi.common.model.ModuleNameV2 import com.navi.common.network.ApiConstants.API_CODE_CONNECT_EXCEPTION import com.navi.common.network.ApiConstants.API_CODE_ERROR +import com.navi.common.network.ApiConstants.API_CODE_SLOW_NETWORK import com.navi.common.network.ApiConstants.API_CODE_SOCKET_TIMEOUT import com.navi.common.network.ApiConstants.API_CODE_UNKNOWN_HOST import com.navi.common.network.ApiConstants.API_ERROR_PEER_UNVERIFIED @@ -282,6 +283,8 @@ fun getErrorTitle(title: String, isNae: Boolean, statusCode: Int): String { statusCode == API_CODE_CONNECT_EXCEPTION || statusCode == API_CODE_UNKNOWN_HOST -> AppServiceManager.application.getString(R.string.no_internet_connection) + statusCode == API_CODE_SLOW_NETWORK -> + AppServiceManager.application.getString(R.string.internet_too_slow) statusCode == API_CODE_ERROR || statusCode == API_CODE_SOCKET_TIMEOUT || statusCode == API_WRONG_ERROR_RESPONSE || @@ -303,6 +306,8 @@ fun getErrorDescription(description: String, isNae: Boolean, statusCode: Int): S AppServiceManager.application.getString( R.string.check_internet_connectivity_and_try_again ) + statusCode == API_CODE_SLOW_NETWORK -> + AppServiceManager.application.getString(R.string.check_internet_connectivity) statusCode == API_CODE_ERROR || statusCode == API_CODE_SOCKET_TIMEOUT || statusCode == API_WRONG_ERROR_RESPONSE || diff --git a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt index 55c709f1c0..3ce20afe5a 100644 --- a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt +++ b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt @@ -138,6 +138,10 @@ object FirebaseRemoteConfigHelper { const val LITMUS_EXPERIMENTS_CACHE_DURATION_IN_MILLIS = "LITMUS_EXPERIMENTS_CACHE_DURATION_IN_MILLIS" const val NAVI_PAY_INTENT_ACTIVITY_CHECK_ENABLED = "NAVI_PAY_INTENT_ACTIVITY_CHECK_ENABLED" + const val NETWORK_SPEED_CHECK_FREQUENCY = "NETWORK_SPEED_CHECK_FREQUENCY" + const val LOW_NETWORK_SIGNAL_THRESHOLD = "LOW_NETWORK_SIGNAL_THRESHOLD" + const val LOW_NETWORK_BANDWIDTH_THRESHOLD = "LOW_NETWORK_BANDWIDTH_THRESHOLD" + const val LOW_NETWORK_SPEED_THRESHOLD = "LOW_NETWORK_SPEED_THRESHOLD" // GI const val NAVI_GI_RETRY_POLICY_ENABLED = "NAVI_GI_RETRY_POLICY_ENABLED" diff --git a/android/navi-common/src/main/java/com/navi/common/network/ApiConstants.kt b/android/navi-common/src/main/java/com/navi/common/network/ApiConstants.kt index 25f9f418aa..08b3071335 100644 --- a/android/navi-common/src/main/java/com/navi/common/network/ApiConstants.kt +++ b/android/navi-common/src/main/java/com/navi/common/network/ApiConstants.kt @@ -1,6 +1,6 @@ /* * - * * Copyright © 2019-2024 by Navi Technologies Limited + * * Copyright © 2019-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ @@ -25,6 +25,7 @@ object ApiConstants { const val API_WRONG_ERROR_RESPONSE = 25 const val API_ERROR_PEER_UNVERIFIED = 26 const val IO_EXCEPTION_ERROR = 27 + const val API_CODE_SLOW_NETWORK = 28 // Server error const val API_SUCCESS_CODE = 200 diff --git a/android/navi-common/src/main/java/com/navi/common/network/NetworkUtil.kt b/android/navi-common/src/main/java/com/navi/common/network/NetworkUtil.kt index 0c8e2b982e..5cf6d61345 100644 --- a/android/navi-common/src/main/java/com/navi/common/network/NetworkUtil.kt +++ b/android/navi-common/src/main/java/com/navi/common/network/NetworkUtil.kt @@ -17,9 +17,12 @@ import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.network.models.ErrorMessage import com.navi.common.network.models.GenericResponse import com.navi.common.utils.CommonNaviAnalytics +import com.navi.common.utils.CommonUtils.isNetworkPoor import com.navi.common.utils.GENERAL_ERROR import com.navi.common.utils.NO_INTERNET +import com.navi.common.utils.NetworkStats import com.navi.common.utils.SOCKET_TIMEOUT +import com.navi.common.utils.getDownloadNetworkStrength import com.navi.common.utils.log import java.net.ConnectException import java.net.SocketTimeoutException @@ -32,6 +35,10 @@ import timber.log.Timber fun handleException(e: Throwable, tag: String? = null): ErrorMessage { Timber.d(e, "Failure during processing") + val signalLevel = NaviTrackEvent.signalInfo.level + val signalType = NaviTrackEvent.signalInfo.type.name + val maxBandwidth = getDownloadNetworkStrength(AppServiceManager.application) + val networkSpeed = NetworkStats.networkSpeed NaviAnalyticsHelper.recordException(e) val errorMessage = ErrorMessage() if (!BaseUtils.isNetworkAvailable(AppServiceManager.application)) { @@ -43,12 +50,20 @@ fun handleException(e: Throwable, tag: String? = null): ErrorMessage { } else if (e is UnknownHostException) { errorMessage.statusCode = ApiConstants.API_CODE_UNKNOWN_HOST } else if (e is SocketTimeoutException) { - errorMessage.statusCode = ApiConstants.API_CODE_SOCKET_TIMEOUT + if (isNetworkPoor(maxBandwidth, networkSpeed, signalLevel, signalType)) { + errorMessage.statusCode = ApiConstants.API_CODE_SLOW_NETWORK + } else { + errorMessage.statusCode = ApiConstants.API_CODE_SOCKET_TIMEOUT + } NaviTrackEvent.trackEvent(CommonNaviAnalytics.API_CONNECTION_TIMEOUT) } else if (e is JsonParseException) { errorMessage.statusCode = ApiConstants.API_WRONG_ERROR_RESPONSE } else if (e is SSLHandshakeException || e is SSLPeerUnverifiedException) { - errorMessage.statusCode = ApiConstants.API_CODE_SSL_HANDSHAKE_EXCEPTION + if (isNetworkPoor(maxBandwidth, networkSpeed, signalLevel, signalType)) { + errorMessage.statusCode = ApiConstants.API_CODE_SLOW_NETWORK + } else { + errorMessage.statusCode = ApiConstants.API_CODE_SSL_HANDSHAKE_EXCEPTION + } } else { errorMessage.statusCode = ApiConstants.API_CODE_ERROR } diff --git a/android/navi-common/src/main/java/com/navi/common/resourcemanager/ResourceManagerUseCase.kt b/android/navi-common/src/main/java/com/navi/common/resourcemanager/ResourceManagerUseCase.kt index b63917091b..800d78418f 100644 --- a/android/navi-common/src/main/java/com/navi/common/resourcemanager/ResourceManagerUseCase.kt +++ b/android/navi-common/src/main/java/com/navi/common/resourcemanager/ResourceManagerUseCase.kt @@ -148,7 +148,11 @@ class ResourceManagerUseCase @Inject constructor(@ApplicationContext context: Co private fun getNetworkQuality(context: Context): ConnectionQuality { val speed = getDownloadNetworkStrength(context) - return when (speed) { + // Convert the network strength from KBps (kilobytes per second) to Kbps (kilobits per + // second) + // as we are using the thresholds in Kbps here. + val speedInKbps = speed * 8f + return when (speedInKbps) { in 0f..500f -> ConnectionQuality.POOR in 501f..1000f -> ConnectionQuality.MODERATE else -> ConnectionQuality.GOOD diff --git a/android/navi-common/src/main/java/com/navi/common/utils/CommonUtils.kt b/android/navi-common/src/main/java/com/navi/common/utils/CommonUtils.kt index 3f8250c2f1..14e4ab0074 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/CommonUtils.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/CommonUtils.kt @@ -35,6 +35,7 @@ import com.google.accompanist.pager.PagerDefaults import com.google.accompanist.pager.PagerState import com.google.gson.JsonParseException import com.navi.alfred.utils.log +import com.navi.analytics.model.SignalType import com.navi.analytics.model.UserLocation import com.navi.analytics.utils.NaviTrackEvent import com.navi.base.AppServiceManager @@ -50,10 +51,15 @@ import com.navi.common.R import com.navi.common.checkmate.utils.getEventNameWithVerticalPrefix import com.navi.common.constants.QA import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.LOW_NETWORK_BANDWIDTH_THRESHOLD +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.LOW_NETWORK_SIGNAL_THRESHOLD +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.LOW_NETWORK_SPEED_THRESHOLD +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.NETWORK_SPEED_CHECK_FREQUENCY import com.navi.common.model.common.ErrorLog import com.navi.common.network.ApiConstants import com.navi.common.network.ApiConstants.API_CODE_CONNECT_EXCEPTION import com.navi.common.network.ApiConstants.API_CODE_ERROR +import com.navi.common.network.ApiConstants.API_CODE_SLOW_NETWORK import com.navi.common.network.ApiConstants.API_CODE_SOCKET_TIMEOUT import com.navi.common.network.ApiConstants.API_CODE_SSL_HANDSHAKE_EXCEPTION import com.navi.common.network.ApiConstants.API_CODE_UNKNOWN_HOST @@ -66,6 +72,10 @@ import com.navi.common.utils.Constants.AM_BIG import com.navi.common.utils.Constants.AM_SMALL import com.navi.common.utils.Constants.DECIMAL import com.navi.common.utils.Constants.MINUS +import com.navi.common.utils.Constants.NETWORK_BANDWIDTH_THRESHOLD_REACHED +import com.navi.common.utils.Constants.NETWORK_SIGNAL_STRENGTH_THRESHOLD_REACHED +import com.navi.common.utils.Constants.NETWORK_SPEED_THRESHOLD_REACHED +import com.navi.common.utils.Constants.NO_NETWORK_THRESHOLDS_REACHED import com.navi.common.utils.Constants.PM_BIG import com.navi.common.utils.Constants.PM_SMALL import com.navi.common.utils.Constants.ZERO_STRING @@ -328,6 +338,10 @@ object CommonUtils { } fun getLocalErrorResponse(exception: Exception): ErrorMessage { + val signalLevel = NaviTrackEvent.signalInfo.level + val signalType = NaviTrackEvent.signalInfo.type.name + val maxBandwidth = getDownloadNetworkStrength(AppServiceManager.application) + val networkSpeed = NetworkStats.networkSpeed val errorMessage = ErrorMessage() errorMessage.exception = exception.message if (!isNetworkAvailable() || exception is ConnectException) { @@ -357,15 +371,29 @@ object CommonUtils { AppServiceManager.application.getString(R.string.internet_too_slow) errorMessage.description = AppServiceManager.application.getString(R.string.no_internet_connection_description) - errorMessage.statusCode = API_CODE_SOCKET_TIMEOUT + if (isNetworkPoor(maxBandwidth, networkSpeed, signalLevel, signalType)) { + errorMessage.statusCode = API_CODE_SLOW_NETWORK + } else { + errorMessage.statusCode = API_CODE_SOCKET_TIMEOUT + } } else if (exception is SSLHandshakeException || exception is SSLPeerUnverifiedException) { val isRestartNeeded = fetchNewKeyFromFirebaseAndRestart() if (isRestartNeeded.not()) { - errorMessage.message = - AppServiceManager.application.getString(R.string.something_went_wrong) - errorMessage.description = - AppServiceManager.application.getString(R.string.technical_issue) - errorMessage.statusCode = API_CODE_SSL_HANDSHAKE_EXCEPTION + if (isNetworkPoor(maxBandwidth, networkSpeed, signalLevel, signalType)) { + errorMessage.message = + AppServiceManager.application.getString(R.string.internet_too_slow) + errorMessage.description = + AppServiceManager.application.getString( + R.string.no_internet_connection_description + ) + errorMessage.statusCode = API_CODE_SLOW_NETWORK + } else { + errorMessage.message = + AppServiceManager.application.getString(R.string.something_went_wrong) + errorMessage.description = + AppServiceManager.application.getString(R.string.technical_issue) + errorMessage.statusCode = API_CODE_SSL_HANDSHAKE_EXCEPTION + } } } else { errorMessage.message = @@ -404,6 +432,18 @@ object CommonUtils { AppServiceManager.application.getString(R.string.no_internet_connection) + "_" } + API_CODE_SLOW_NETWORK -> { + errorMessage.message = + AppServiceManager.application.getString(R.string.internet_too_slow) + errorMessage.description = + AppServiceManager.application.getString( + R.string.no_internet_connection_description + ) + errorMessage.trace = + (errorMessage.trace ?: "") + + AppServiceManager.application.getString(R.string.internet_too_slow) + + "_" + } API_CODE_ERROR, API_CODE_SOCKET_TIMEOUT, API_WRONG_ERROR_RESPONSE, @@ -433,6 +473,96 @@ object CommonUtils { return errorMessage } + /** + * Determines if the current network condition is poor based on bandwidth, speed, and signal + * strength if the frequency for network speed checks is enabled + * + * @param maxBandwidth The maximum bandwidth of the network connection in KBps. + * @param networkSpeed The current network speed in Bps. + * @param signalLevel The signal strength level (e.g., WiFi signal strength or cellular signal + * strength). + * @param signalType The type of signal (e.g., WiFi, CELLULAR, or UNKNOWN). + * @return `true` if any of the following conditions are met: + * - The maximum bandwidth is below the configured low network bandwidth threshold. + * - The current network speed is below the configured low network speed threshold. + * - The signal level is below the configured low network signal threshold, or the signal + * type is unknown. Otherwise, returns `false`. + * + * Note: + * - + * - If the network speed check frequency is set to 0 or negative, the check is skipped + * entirely. + * - Threshold values for bandwidth, speed, and signal are fetched dynamically using + * `FirebaseRemoteConfigHelper`. + * - If any of these thresholds are set to 0 or negative, that specific check is ignored. + */ + fun isNetworkPoor( + maxBandwidth: Float, + networkSpeed: Float, + signalLevel: Int, + signalType: String, + ): Boolean { + val lowNetworkSpeedThreshold = + FirebaseRemoteConfigHelper.getLong(LOW_NETWORK_SPEED_THRESHOLD) + val lowNetworkSignalThreshold = + FirebaseRemoteConfigHelper.getLong(LOW_NETWORK_SIGNAL_THRESHOLD) + val lowNetworkBandwidthThreshold = + FirebaseRemoteConfigHelper.getLong(LOW_NETWORK_BANDWIDTH_THRESHOLD) + val eventAttributes = + mapOf( + "networkSpeed" to networkSpeed.toString(), + "lowNetworkSpeedThreshold" to lowNetworkSpeedThreshold.toString(), + "signalLevel" to signalLevel.toString(), + "signalType" to signalType, + "lowNetworkSignalThreshold" to lowNetworkSignalThreshold.toString(), + "maxBandwidth" to maxBandwidth.toString(), + "lowNetworkBandwidthThreshold" to lowNetworkBandwidthThreshold.toString(), + ) + return when { + // Check if network speed checks are disabled or network speed is invalid + FirebaseRemoteConfigHelper.getLong(NETWORK_SPEED_CHECK_FREQUENCY) <= 0 || + networkSpeed < 0 -> false + + // Network speed is below the threshold + lowNetworkSpeedThreshold > 0 && networkSpeed < lowNetworkSpeedThreshold -> { + NaviTrackEvent.trackEvent( + eventName = NETWORK_SPEED_THRESHOLD_REACHED, + eventValues = eventAttributes, + ) + true + } + + // Signal strength is below the threshold or unknown + lowNetworkSignalThreshold > 0 && + (signalLevel < lowNetworkSignalThreshold || + signalType == SignalType.UNKNOWN.name) -> { + NaviTrackEvent.trackEvent( + eventName = NETWORK_SIGNAL_STRENGTH_THRESHOLD_REACHED, + eventValues = eventAttributes, + ) + true + } + + // Bandwidth is below the threshold + lowNetworkBandwidthThreshold > 0 && maxBandwidth < lowNetworkBandwidthThreshold -> { + NaviTrackEvent.trackEvent( + eventName = NETWORK_BANDWIDTH_THRESHOLD_REACHED, + eventValues = eventAttributes, + ) + true + } + + // No thresholds are reached + else -> { + NaviTrackEvent.trackEvent( + eventName = NO_NETWORK_THRESHOLDS_REACHED, + eventValues = eventAttributes, + ) + false + } + } + } + fun String.formattedCurrency(): String { if (this.isBlank()) return EMPTY val parts = this.split(DECIMAL) diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt index 7becf80e55..9e6a6a81fa 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt @@ -328,6 +328,13 @@ object Constants { const val IS_SUCCESS = "is_success" const val RETRY_COUNT = "retry_count" + const val NETWORK_SPEED_TRACKING_EXCEPTION = "dev_network_speed_tracking_exception" + const val NETWORK_SPEED_THRESHOLD_REACHED = "dev_network_speed_threshold_reached" + const val NETWORK_BANDWIDTH_THRESHOLD_REACHED = "dev_network_bandwidth_threshold_reached" + const val NETWORK_SIGNAL_STRENGTH_THRESHOLD_REACHED = + "dev_network_signal_strength_threshold_reached" + const val NO_NETWORK_THRESHOLDS_REACHED = "dev_no_network_thresholds_reached" + // navi pay common cache keys const val NAVI_PAY_REWARDS_NUDGE_CACHE_KEY = "naviPayRewardsNudge" diff --git a/android/navi-common/src/main/java/com/navi/common/utils/GenericErrorData.kt b/android/navi-common/src/main/java/com/navi/common/utils/GenericErrorData.kt index ddd9aa2e52..20aec287f8 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/GenericErrorData.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/GenericErrorData.kt @@ -30,12 +30,12 @@ fun getErrorData( showFullScreenError: Boolean = false, ): GenericErrorResponse { return GenericErrorResponse( - listOf(Action(title ?: context.getString(R.string.retry))), - AssetDetails(GENERAL_ERROR_ICON), - context.getString(R.string.technical_issue), - context.getString(R.string.something_went_wrong), - null, - GENERAL_ERROR, + actions = listOf(Action(title ?: context.getString(R.string.retry))), + assetDetails = AssetDetails(GENERAL_ERROR_ICON), + message = context.getString(R.string.technical_issue), + title = context.getString(R.string.something_went_wrong), + optionalNote = null, + code = GENERAL_ERROR, statusCode = statusCode, apiUrl = apiUrl, logMessage = logMessage, @@ -53,12 +53,12 @@ fun getApiFailedData( showFullScreenError: Boolean = false, ): GenericErrorResponse { return GenericErrorResponse( - listOf(Action(context.getString(R.string.retry))), - AssetDetails(GENERIC_ERROR), - context.getString(R.string.technical_issue), - context.getString(R.string.something_went_wrong), - null, - GENERAL_ERROR, + actions = listOf(Action(context.getString(R.string.retry))), + assetDetails = AssetDetails(GENERIC_ERROR), + message = context.getString(R.string.technical_issue), + title = context.getString(R.string.something_went_wrong), + optionalNote = null, + code = GENERAL_ERROR, statusCode = statusCode, apiUrl = apiUrl, logMessage = logMessage, @@ -76,12 +76,35 @@ fun getNoInternetData( showFullScreenError: Boolean = false, ): GenericErrorResponse { return GenericErrorResponse( - listOf(Action(context.getString(R.string.retry))), - AssetDetails(WIFI_ERROR_ICON), - context.getString(R.string.check_internet_connectivity_and_try_again), - context.getString(R.string.no_internet_connection), - null, - NO_INTERNET, + actions = listOf(Action(context.getString(R.string.retry))), + assetDetails = AssetDetails(WIFI_ERROR_ICON), + message = context.getString(R.string.check_internet_connectivity_and_try_again), + title = context.getString(R.string.no_internet_connection), + optionalNote = null, + code = NO_INTERNET, + statusCode = statusCode, + apiUrl = apiUrl, + logMessage = logMessage, + errorMetaData = errorMetaData, + showFullScreenError = showFullScreenError, + ) +} + +fun getSlowInternetData( + context: Context, + statusCode: Int? = null, + apiUrl: String? = null, + logMessage: String? = null, + errorMetaData: ErrorMetaData? = null, + showFullScreenError: Boolean = false, +): GenericErrorResponse { + return GenericErrorResponse( + actions = listOf(Action(context.getString(R.string.retry))), + assetDetails = AssetDetails(WIFI_ERROR_ICON), + message = context.getString(R.string.check_internet_connectivity), + title = context.getString(R.string.internet_too_slow), + optionalNote = null, + code = SLOW_INTERNET, statusCode = statusCode, apiUrl = apiUrl, logMessage = logMessage, @@ -98,12 +121,12 @@ fun getSocketTimeOutData( errorMetaData: ErrorMetaData? = null, ): GenericErrorResponse { return GenericErrorResponse( - listOf(Action(context.getString(R.string.retry))), - AssetDetails(WIFI_ERROR_ICON), - context.getString(R.string.socket_time_description), - context.getString(R.string.socket_time_title), - null, - SOCKET_TIMEOUT, + actions = listOf(Action(context.getString(R.string.retry))), + assetDetails = AssetDetails(WIFI_ERROR_ICON), + message = context.getString(R.string.socket_time_description), + title = context.getString(R.string.socket_time_title), + optionalNote = null, + code = SOCKET_TIMEOUT, statusCode = statusCode, apiUrl = apiUrl, logMessage = logMessage, @@ -189,6 +212,7 @@ fun getEditAccountData(context: Context): GenericWarningResponse { const val GENERAL_ERROR = "generic_error_screen" const val NO_INTERNET = "internet_connectivity_error_screen" +const val SLOW_INTERNET = "slow_internet_error_screen" const val SOCKET_TIMEOUT = "socket_timeout" const val HL_NO_BANK_DISCOVERED = "hl_no_bank_discovered" const val AADHAR_VERIFICATION_CANCELED = "AADHAR_VERIFICATION_CANCELED" diff --git a/android/navi-common/src/main/java/com/navi/common/utils/NetworkStats.kt b/android/navi-common/src/main/java/com/navi/common/utils/NetworkStats.kt new file mode 100644 index 0000000000..71bf4aa790 --- /dev/null +++ b/android/navi-common/src/main/java/com/navi/common/utils/NetworkStats.kt @@ -0,0 +1,114 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.common.utils + +import android.net.TrafficStats +import com.navi.analytics.utils.NaviTrackEvent +import com.navi.common.utils.Constants.NETWORK_SPEED_TRACKING_EXCEPTION +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +object NetworkStats { + var networkSpeed: Float = -1f + private val speedHistory = mutableListOf() + private const val SMOOTHING_WINDOW = 5 + private var trackingJob: Job? = null + private var isTracking = false + + private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> + if (isTracking) { + stopTracking() + } + NaviTrackEvent.trackEvent( + eventName = NETWORK_SPEED_TRACKING_EXCEPTION, + eventValues = mapOf("coroutineException" to exception.message.orEmpty()), + ) + exception.log() + } + + private fun getTotalReceptionBytes(): Long { + try { + return TrafficStats.getTotalRxBytes() + } catch (e: Exception) { + stopTracking() + NaviTrackEvent.trackEvent( + eventName = NETWORK_SPEED_TRACKING_EXCEPTION, + eventValues = mapOf("exception" to e.message.orEmpty()), + ) + e.log() + return -1L + } + } + + /** + * Starts tracking the network download speed in bytes per second (Bps) and updates it + * periodically. + * + * @param updateIntervalMs The interval in milliseconds at which the network speed is updated. + * Default is 1000ms (1 second). + * + * Functionality: + * - Continuously calculates the instantaneous network speed based on data received over time. + * - Maintains a history of recent speeds, smoothing the speed value using a rolling average + * over the last `SMOOTHING_WINDOW` values. + * - Updates the `networkSpeed` variable with the averaged speed in real-time. + * - Uses `TrafficStats.getTotalRxBytes()` to measure total bytes received since device boot. + * + * Guardrails and Precautions: + * - Prevents multiple instances of tracking using the `isTracking` flag. + * - Checks if `TrafficStats` is unsupported on the device; exits early if so. + * - Handles exceptions gracefully and logs them via `NaviAnalyticsHelper.recordException`. + * + * Notes: + * - Runs on a coroutine in the `Dispatchers.IO` context to avoid blocking the main thread. + */ + fun startTracking(updateIntervalMs: Long = 1000L) { + if ( + isTracking || + getTotalReceptionBytes() == TrafficStats.UNSUPPORTED.toLong() || + updateIntervalMs <= 0 + ) + return + isTracking = true + + var previousRxBytes = getTotalReceptionBytes() + var previousTime = System.currentTimeMillis() + + trackingJob = + CoroutineScope(Dispatchers.IO + coroutineExceptionHandler).launch { + while (isTracking) { + val currentTime = System.currentTimeMillis() + val elapsedTime = (currentTime - previousTime) / 1000f + val currentRxBytes = getTotalReceptionBytes() + val downloadedBytes = currentRxBytes - previousRxBytes + val instantaneousSpeed = + (downloadedBytes / elapsedTime).takeIf { elapsedTime > 0 } ?: 0f + speedHistory.add(instantaneousSpeed) + if (speedHistory.size > SMOOTHING_WINDOW) { + speedHistory.removeAt(0) + } + networkSpeed = speedHistory.average().toFloat() + previousTime = currentTime + previousRxBytes = currentRxBytes + delay(updateIntervalMs) + } + } + } + + fun stopTracking() { + isTracking = false + trackingJob?.cancel() + trackingJob = null + speedHistory.clear() + networkSpeed = -1f + } +} diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt b/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt index fc520ccd7f..22b8e87f9c 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt @@ -375,6 +375,7 @@ fun getGlobalErrorType(errorCode: Int?): String { return if ( errorCode == ApiConstants.NO_INTERNET || errorCode == ApiConstants.API_CODE_SOCKET_TIMEOUT || + errorCode == ApiConstants.API_CODE_SLOW_NETWORK || errorCode == ApiConstants.API_CODE_ERROR || errorCode == ApiConstants.API_ERROR_NO_USER_FOUND || errorCode == ApiConstants.API_NOT_FOUND || @@ -431,13 +432,26 @@ fun copyToClipboard( Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show() } +/** + * Retrieves the estimated maximum downstream bandwidth of the current network connection. + * + * @param context The application context used to access the ConnectivityManager service. + * @return The downstream bandwidth in kilo Bytes per second (KBps) as a Float. Returns 0f if the + * bandwidth cannot be determined or an exception occurs. + * + * Note: + * - The bandwidth always only refers to the estimated transport bandwidth for the first hop. + * - If an exception occurs during retrieval, it is logged and 0f is returned. + * - This method uses `linkDownstreamBandwidthKbps` from `NetworkCapabilities` to get the bandwidth. + */ fun getDownloadNetworkStrength(context: Context): Float { try { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - return networkCapabilities?.linkDownstreamBandwidthKbps?.toFloat() ?: 0f + val networkBandwidth = networkCapabilities?.linkDownstreamBandwidthKbps?.toFloat() ?: 0f + return networkBandwidth / 8f } catch (e: Exception) { e.log() } diff --git a/android/navi-common/src/main/java/com/navi/common/viewmodel/BaseVM.kt b/android/navi-common/src/main/java/com/navi/common/viewmodel/BaseVM.kt index 762a700b2c..5a0fa5a93c 100644 --- a/android/navi-common/src/main/java/com/navi/common/viewmodel/BaseVM.kt +++ b/android/navi-common/src/main/java/com/navi/common/viewmodel/BaseVM.kt @@ -19,6 +19,7 @@ import com.navi.common.network.ApiConstants.API_BAD_GATEWAY import com.navi.common.network.ApiConstants.API_BAD_REQUEST import com.navi.common.network.ApiConstants.API_CODE_CONNECT_EXCEPTION import com.navi.common.network.ApiConstants.API_CODE_ERROR +import com.navi.common.network.ApiConstants.API_CODE_SLOW_NETWORK import com.navi.common.network.ApiConstants.API_CODE_SOCKET_TIMEOUT import com.navi.common.network.ApiConstants.API_CODE_UNKNOWN_HOST import com.navi.common.network.ApiConstants.API_ERROR_NO_USER_FOUND @@ -37,6 +38,7 @@ import com.navi.common.utils.CommonNaviAnalytics import com.navi.common.utils.getApiFailedData import com.navi.common.utils.getErrorData import com.navi.common.utils.getNoInternetData +import com.navi.common.utils.getSlowInternetData import com.navi.common.utils.getSocketTimeOutData import com.navi.common.utils.isNetworkAvailable import com.navi.uitron.viewmodel.UiTronViewModel @@ -153,11 +155,24 @@ abstract class BaseVM( cancelable, ) } + API_CODE_SLOW_NETWORK -> { + Triple( + getSlowInternetData( + AppServiceManager.application, + error.statusCode, + apiUrl = error.apiUrl, + logMessage = error.message, + errorMetaData = errorMetaData, + ), + errorTag, + cancelable, + ) + } API_CODE_SOCKET_TIMEOUT -> { Triple( getSocketTimeOutData( - AppServiceManager.application, - error.statusCode, + context = AppServiceManager.application, + statusCode = error.statusCode, apiUrl = error.apiUrl, logMessage = error.message, errorMetaData = errorMetaData, diff --git a/android/navi-common/src/main/res/xml/default_remote_config.xml b/android/navi-common/src/main/res/xml/default_remote_config.xml index 5ac41a40f4..c78222c6ea 100644 --- a/android/navi-common/src/main/res/xml/default_remote_config.xml +++ b/android/navi-common/src/main/res/xml/default_remote_config.xml @@ -658,4 +658,20 @@ IS_FESTIVE_APP_ICON_ENABLED false + + NETWORK_SPEED_CHECK_FREQUENCY + 1000 + + + LOW_NETWORK_SPEED_THRESHOLD + 0 + + + LOW_NETWORK_BANDWIDTH_THRESHOLD + 0 + + + LOW_NETWORK_SIGNAL_THRESHOLD + 0 + \ No newline at end of file diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/health/fragment/ErrorFragment.kt b/android/navi-insurance/src/main/java/com/navi/insurance/health/fragment/ErrorFragment.kt index 5b6a5cae55..39539fc1e8 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/health/fragment/ErrorFragment.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/health/fragment/ErrorFragment.kt @@ -17,6 +17,7 @@ import com.navi.base.deeplink.util.DeeplinkConstants import com.navi.base.model.CtaData import com.navi.base.sharedpref.PreferenceManager import com.navi.common.network.ApiConstants.API_CODE_ERROR +import com.navi.common.network.ApiConstants.API_CODE_SLOW_NETWORK import com.navi.common.network.ApiConstants.API_CODE_SOCKET_TIMEOUT import com.navi.common.network.ApiConstants.NO_INTERNET import com.navi.common.network.models.ErrorMessage @@ -73,6 +74,7 @@ class ErrorFragment : BaseFragment(), View.OnClickListener { binding.errorDescription.text = "Please check your internet connectivity and try again" } + API_CODE_SLOW_NETWORK, API_CODE_SOCKET_TIMEOUT -> { binding.networkError.visibility = View.VISIBLE binding.errorTitle.text = "Internet is slow" diff --git a/android/navi-insurance/src/main/java/com/navi/insurance/util/Utility.kt b/android/navi-insurance/src/main/java/com/navi/insurance/util/Utility.kt index 43b1719e0b..e0068bb4d2 100644 --- a/android/navi-insurance/src/main/java/com/navi/insurance/util/Utility.kt +++ b/android/navi-insurance/src/main/java/com/navi/insurance/util/Utility.kt @@ -42,6 +42,7 @@ import com.navi.common.checkmate.core.CheckMateManager import com.navi.common.checkmate.model.MetricInfo import com.navi.common.checkmate.model.MetricSource import com.navi.common.network.ApiConstants.API_CODE_ERROR +import com.navi.common.network.ApiConstants.API_CODE_SLOW_NETWORK import com.navi.common.network.ApiConstants.API_CODE_SOCKET_TIMEOUT import com.navi.common.network.ApiConstants.NO_INTERNET import com.navi.common.network.models.ErrorMessage @@ -406,6 +407,7 @@ fun getGlobalErrorType(errorCode: Int?): String { return if ( errorCode == NO_INTERNET || errorCode == API_CODE_SOCKET_TIMEOUT || + errorCode == API_CODE_SLOW_NETWORK || errorCode == API_CODE_ERROR || errorCode == ApiConstants.API_ERROR_NO_USER_FOUND || errorCode == ApiConstants.API_NOT_FOUND ||