diff --git a/android/app/src/main/java/com/naviapp/home/common/handler/EffectHandler.kt b/android/app/src/main/java/com/naviapp/home/common/handler/EffectHandler.kt index 46476d56af..b1717bc87e 100644 --- a/android/app/src/main/java/com/naviapp/home/common/handler/EffectHandler.kt +++ b/android/app/src/main/java/com/naviapp/home/common/handler/EffectHandler.kt @@ -57,7 +57,8 @@ constructor( is HpEffects.OnActionsFromJson -> handleActionsFromJson(effects, homeVM) is HpEffects.TrackBottomBarEvents -> trackBottomBarEvents() is HpEffects.OnNotificationUpdatedCount -> handleNotificationCount(effects, homeVM) - is HpEffects.OnRenderActions -> handleRenderActions(homeVM) + is HpEffects.OnPostRenderActions -> handlePostRenderActions(homeVM) + is HpEffects.OnApiSuccessRenderActions -> handleApiSuccessRenderActions(homeVM) is HpEffects.OnApiFailure -> handleApiFailure(effects, homeVM) is HpEffects.LogAppLaunchTime -> logAppLaunchTime(effects) is HpEffects.InitiatePayment -> onPaymentInitiated(effects.data) @@ -148,11 +149,12 @@ constructor( ?.let { homeVM.handleAction(it) } } - private fun handleRenderActions(homeVM: HomeViewModel) { - homeVM.state.value.renderActions?.let { renderActions -> - renderActions.postRenderAction?.let { homeVM.handleActions(it) } - renderActions.apiSuccessRenderAction?.let { homeVM.handleActions(it) } - } + private fun handlePostRenderActions(homeVM: HomeViewModel) { + homeVM.state.value.renderActions?.postRenderAction?.let { homeVM.handleActions(it) } + } + + private fun handleApiSuccessRenderActions(homeVM: HomeViewModel) { + homeVM.state.value.renderActions?.apiSuccessRenderAction.let { homeVM.handleActions(it) } } private fun handleApiFailure(effects: HpEffects.OnApiFailure, homeVM: HomeViewModel) { diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/utils/HomeScreenHelper.kt b/android/app/src/main/java/com/naviapp/home/compose/home/utils/HomeScreenHelper.kt index d17db2706b..5a46bbeaa3 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/home/utils/HomeScreenHelper.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/home/utils/HomeScreenHelper.kt @@ -48,7 +48,6 @@ fun InitLifecycleListener(homeVM: () -> HomeViewModel, activity: HomePageActivit } Lifecycle.Event.ON_RESUME -> { if (homeVM().shouldRefreshHomeApi) { - homeVM().setEffect { HpEffects.OnRenderActions } homeVM().setEffect { HpEffects.FetchHomeApi } } resetImpressionStates(homeVM()) diff --git a/android/app/src/main/java/com/naviapp/home/model/HomeScreenContentResult.kt b/android/app/src/main/java/com/naviapp/home/model/HomeScreenContentResult.kt new file mode 100644 index 0000000000..74d5c1ce95 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/model/HomeScreenContentResult.kt @@ -0,0 +1,20 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.home.model + +import com.navi.base.cache.model.NaviCacheEntity + +sealed interface HomeScreenContentResult { + data class Success( + val data: NaviCacheEntity? = null, + val isDataSame: Boolean, + val isScreenHashSame: Boolean = false, + ) : HomeScreenContentResult + + data class Error(val isCacheAvailable: Boolean) : HomeScreenContentResult +} diff --git a/android/app/src/main/java/com/naviapp/home/model/SelectiveRefreshState.kt b/android/app/src/main/java/com/naviapp/home/model/SelectiveRefreshState.kt new file mode 100644 index 0000000000..82eb252c9b --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/model/SelectiveRefreshState.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.home.model + +enum class SelectiveRefreshState { + LOADING, + SUCCESS, + ERROR, +} diff --git a/android/app/src/main/java/com/naviapp/home/reducer/HomeReducer.kt b/android/app/src/main/java/com/naviapp/home/reducer/HomeReducer.kt index 101c8c9e6a..69bd6a6e46 100644 --- a/android/app/src/main/java/com/naviapp/home/reducer/HomeReducer.kt +++ b/android/app/src/main/java/com/naviapp/home/reducer/HomeReducer.kt @@ -207,7 +207,9 @@ sealed interface HpEffects : UiEffect { data class OnNotificationUpdatedCount(val count: Int) : HpEffects - data object OnRenderActions : HpEffects + data object OnPostRenderActions : HpEffects + + data object OnApiSuccessRenderActions : HpEffects data class OnApiFailure( val errorMessage: ErrorMessage?, diff --git a/android/app/src/main/java/com/naviapp/home/usecase/FetchHomeItemsUseCase.kt b/android/app/src/main/java/com/naviapp/home/usecase/FetchHomeItemsUseCase.kt index c9703b0d86..365a12d850 100644 --- a/android/app/src/main/java/com/naviapp/home/usecase/FetchHomeItemsUseCase.kt +++ b/android/app/src/main/java/com/naviapp/home/usecase/FetchHomeItemsUseCase.kt @@ -17,7 +17,7 @@ import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.network.models.ErrorMessage import com.navi.common.network.models.GenericErrorResponse import com.navi.common.network.models.RepoResult -import com.navi.common.network.models.isSuccess +import com.navi.common.network.models.isSuccessWithData import com.naviapp.BuildConfig import com.naviapp.app.NaviApplication import com.naviapp.home.respository.HomeRepository @@ -79,18 +79,55 @@ constructor( onFailure: suspend (apiErrorMessage: ErrorMessage?, errors: List?) -> Unit, ): NaviCacheAltSourceEntity { - return if (connectivityObserver.isInternetConnected()) { - response.data?.screenStructure?.let { + + return when { + // No internet case + !connectivityObserver.isInternetConnected() -> { + return handleNoInternet(response, noInternetCallback, onFailure) + } + // When response is error or has no data + !response.isSuccessWithData() -> { + onFailure(response.error, response.errors) + return NaviCacheAltSourceEntity(isSuccess = false) + } + // When the response is success but screenStructure is null + response.data?.screenStructure == null -> { + // Check for the case when screenStructure is present in the response + // [Same Screen Hash Case] + if (response.data?.screenMetaData != null) { + NaviCacheAltSourceEntity(isSuccess = true) + } else { + onFailure( + response.error?.copy(message = SCREEN_STRUCTURE_AND_META_DATA_NULL), + response.errors, + ) + return NaviCacheAltSourceEntity(isSuccess = false) + } + } + // When the response is success with Data + else -> { NaviCacheAltSourceEntity( - value = dataSerializers.toJson(response.data), + value = response.data?.let { dataSerializers.toJson(it) }, version = BuildConfig.VERSION_CODE, - isSuccess = response.isSuccess(), + isSuccess = true, ) - } ?: NaviCacheAltSourceEntity(isSuccess = false) - } else { - noInternetCallback() - onFailure(response.error, response.errors) - NaviCacheAltSourceEntity(isSuccess = false) + } } } + + private suspend fun handleNoInternet( + response: RepoResult, + noInternetCallback: () -> Unit, + onFailure: + suspend (apiErrorMessage: ErrorMessage?, errors: List?) -> Unit, + ): NaviCacheAltSourceEntity { + noInternetCallback() + onFailure(response.error, response.errors) + return NaviCacheAltSourceEntity(isSuccess = false) + } + + companion object { + const val SCREEN_STRUCTURE_AND_META_DATA_NULL = + "Both screen structure and meta data is null" + } } diff --git a/android/app/src/main/java/com/naviapp/home/usecase/HomeContentProcessingUseCase.kt b/android/app/src/main/java/com/naviapp/home/usecase/HomeContentProcessingUseCase.kt index 82c1d02714..ae5beb3a2b 100644 --- a/android/app/src/main/java/com/naviapp/home/usecase/HomeContentProcessingUseCase.kt +++ b/android/app/src/main/java/com/naviapp/home/usecase/HomeContentProcessingUseCase.kt @@ -10,13 +10,13 @@ package com.naviapp.home.usecase import com.navi.analytics.utils.NaviTrackEvent import com.navi.base.cache.model.NaviCacheAltSourceEntity import com.navi.base.cache.model.NaviCacheEntity -import com.navi.base.cache.model.NaviCacheEntityInfo import com.navi.base.cache.repository.NaviCacheRepositoryImpl import com.navi.base.cache.util.NaviSharedDbKeys import com.navi.common.alchemist.model.AlchemistScreenDefinition import com.navi.common.utils.safeAsync import com.navi.common.utils.safeLaunch -import com.naviapp.BuildConfig +import com.naviapp.home.model.HomeScreenContentResult +import com.naviapp.home.model.SelectiveRefreshState import com.naviapp.home.reducer.HpEvents import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -31,60 +31,181 @@ constructor( ) { companion object { private const val EVENT_NAME = "naviapp_home_content_processing_event" - private const val EVENT_KEY_CURRENT_STATUS = "currentStatus" - // Cache related events - private const val CACHE_RETRIEVAL_NO_CONTENT = "Cache retrieval - No cached content found" - private const val CACHE_CONTENT_PROCESSING_START = "Starting cache content processing" - private const val CACHE_PRIORITY_SECTION_EXTRACTED = "Priority section extracted" - private const val CACHE_SCREEN_LAYOUT_PROCESSED = "Screen layout processed" - private const val CACHE_REMAINING_CONTENT_PROCESSED = "Remaining content processed" - private const val CACHE_CONTENT_PROCESSING_COMPLETED = "Cache content processing completed" - private const val CACHE_DATA_NOT_FOUND = "Home cache data not found during first launch" - private const val CACHE_DATA_LOADED = - "Home cache data loaded successfully during first launch" - private const val CACHE_DATA_RETRIEVAL_START = "Starting cache data retrieval" + // Event Types + private const val EVENT_TYPE = "event_type" + private const val TYPE_SYNC_START = "sync_start" + private const val TYPE_FIRST_TIME_SYNC = "first_time_sync" + private const val TYPE_INCREMENTAL_SYNC = "incremental_sync" + private const val TYPE_CACHE_LOAD = "cache_load" + private const val TYPE_CACHE_PROCESS = "cache_process" + private const val TYPE_NETWORK_SYNC = "network_sync" + private const val TYPE_CONTENT_COMPARE = "content_compare" + private const val TYPE_SCREEN_UPDATE = "screen_update" + private const val TYPE_NETWORK_ERROR = "network_error" - // Network related events - private const val NETWORK_FETCH_START = "Starting network data fetch" - private const val NETWORK_SUBSEQUENT_FETCH_START = "Starting subsequent network fetch" - private const val NETWORK_RESPONSE_NULL = "Network response is null - Context: %s" - private const val NETWORK_ERROR = "Network error - Context: %s" - private const val NETWORK_SUCCESS = "Network success - Context: %s" + // Event Parameters + private const val PARAM_IS_FIRST_TIME = "is_first_time" + private const val PARAM_CACHE_STATUS = "cache_status" + private const val PARAM_IS_DATA_SAME = "is_data_same" + private const val PARAM_SYNC_STATUS = "sync_status" + private const val PARAM_ERROR_TYPE = "error_type" + private const val PARAM_CONTENT_STATUS = "content_status" - // Sync related events - private const val SYNC_STARTED = "Home sync started - First time render: %s" - private const val SYNC_CACHE_OPERATION_COMPLETED = - "Cache operation completed - Data loaded: %s" - private const val SYNC_FIRST_RENDER_NETWORK = "First render completed from network data" - private const val SYNC_REFRESH_CONTENT = - "Refreshing content - First render: %s, Widgets: %d" + // Event Values + private const val VALUE_NOT_FOUND = "not_found" + private const val VALUE_LOADED = "loaded" + private const val VALUE_HASH_UNCHANGED = "hash_unchanged" + private const val VALUE_EMPTY_CONTENT = "empty_content" + private const val VALUE_CONTENT_UPDATED = "content_updated" + private const val VALUE_PRIORITY_SECTION_UPDATED = "priority_section_updated" + private const val VALUE_SCREEN_LAYOUT_UPDATED = "screen_layout_updated" + private const val VALUE_REMAINING_CONTENT_UPDATED = "remaining_content_updated" + private const val VALUE_INCREMENTAL_SYNC = "incremental_sync" + private const val VALUE_NETWORK_SYNC = "network_sync" + private const val VALUE_ALT_SOURCE_FETCH = "alt_source_fetch" } - private fun statusTrackEvent(event: String) { - NaviTrackEvent.trackEvent( - EVENT_NAME, - eventValues = mapOf(EVENT_KEY_CURRENT_STATUS to event), - ) + private fun trackEvent(type: String, params: Map = emptyMap()) { + val eventParams = mutableMapOf() + eventParams[EVENT_TYPE] = type + eventParams.putAll(params) + NaviTrackEvent.trackEvent(EVENT_NAME, eventParams) } - private fun statusTrackEventWithContext(eventTemplate: String, vararg args: Any) { - statusTrackEvent(eventTemplate.format(*args)) - } - - private fun refreshHomeScreenContent( - screenDefinition: AlchemistScreenDefinition, + context(CoroutineScope) + fun syncHomeContentData( + isFirstTimeRender: Boolean, + fetchHomeContent: suspend () -> NaviCacheAltSourceEntity, + onContentSynchronized: () -> Unit, + onHomeFetchSuccessFromNetwork: () -> Unit, + onSelectiveRefreshStateChange: (SelectiveRefreshState) -> Unit, eventHandler: (HpEvents) -> Unit, - isRenderingFirstTime: Boolean, ) { - eventHandler(HpEvents.UpdateScreenWithoutContent(screenDefinition)) + trackEvent(TYPE_SYNC_START, mapOf(PARAM_IS_FIRST_TIME to isFirstTimeRender.toString())) - screenDefinition.screenStructure?.content?.widgets?.let { homeWidgets -> - eventHandler(HpEvents.UpdateFrontLayerContent(homeWidgets, isRenderingFirstTime)) + if (isFirstTimeRender) { + initializeFirstTimeContentSync( + fetchHomeContent, + onContentSynchronized, + eventHandler, + onSelectiveRefreshStateChange, + onHomeFetchSuccessFromNetwork, + ) + } else { + performIncrementalContentSync( + fetchHomeContent, + onContentSynchronized, + eventHandler, + onSelectiveRefreshStateChange, + onHomeFetchSuccessFromNetwork, + ) } } - private suspend fun retrieveHomeCacheData( + private fun CoroutineScope.initializeFirstTimeContentSync( + fetchHomeContent: suspend () -> NaviCacheAltSourceEntity, + onContentSynchronized: () -> Unit, + eventHandler: (HpEvents) -> Unit, + onSelectiveRefreshStateChange: (SelectiveRefreshState) -> Unit, + onHomeFetchSuccessFromNetwork: () -> Unit, + ) { + var cacheDataLoaded = false + trackEvent(TYPE_FIRST_TIME_SYNC) + + val cacheLoadOperation = + initializeCacheDataLoad( + eventHandler = eventHandler, + onContentSynchronized = onContentSynchronized, + onCacheLoaded = { cacheDataLoaded = it }, + ) + + initiateNetworkSyncAndProcess( + fetchHomeContent = fetchHomeContent, + cacheLoadOperation = cacheLoadOperation, + cacheDataLoaded = cacheDataLoaded, + onContentSynchronized = onContentSynchronized, + eventHandler = eventHandler, + onSelectiveRefreshStateChange = onSelectiveRefreshStateChange, + onHomeFetchSuccessFromNetwork = onHomeFetchSuccessFromNetwork, + ) + } + + private fun CoroutineScope.performIncrementalContentSync( + fetchHomeContent: suspend () -> NaviCacheAltSourceEntity, + onContentSynchronized: () -> Unit, + eventHandler: (HpEvents) -> Unit, + onSelectiveRefreshStateChange: (SelectiveRefreshState) -> Unit, + onHomeFetchSuccessFromNetwork: () -> Unit, + ) { + trackEvent(TYPE_INCREMENTAL_SYNC) + + safeLaunch(Dispatchers.IO) { + val networkResponse = + fetchAndCompareAltSourceContent( + key = NaviSharedDbKeys.HOME_TAB.name, + getDataFromAltSource = fetchHomeContent, + ) + + handleNetworkSyncResponse( + networkResponse = networkResponse, + eventHandler = eventHandler, + onNetworkError = { + trackEvent( + TYPE_NETWORK_ERROR, + mapOf(PARAM_ERROR_TYPE to VALUE_INCREMENTAL_SYNC), + ) + onSelectiveRefreshStateChange(SelectiveRefreshState.ERROR) + }, + onScreenHashUnchanged = { + trackEvent( + TYPE_CONTENT_COMPARE, + mapOf(PARAM_CONTENT_STATUS to VALUE_HASH_UNCHANGED), + ) + onHomeFetchSuccessFromNetwork() + onSelectiveRefreshStateChange(SelectiveRefreshState.SUCCESS) + }, + onScreenContentUpdated = { screenContent, isDataUnchanged -> + trackEvent( + TYPE_SCREEN_UPDATE, + mapOf(PARAM_IS_DATA_SAME to isDataUnchanged.toString()), + ) + updateScreenContent( + screenContent = screenContent, + eventHandler = eventHandler, + isFirstRender = false, + ) + onContentSynchronized() + if (!isDataUnchanged) { + onHomeFetchSuccessFromNetwork() + } + onSelectiveRefreshStateChange(SelectiveRefreshState.SUCCESS) + }, + ) + } + } + + private fun CoroutineScope.initializeCacheDataLoad( + eventHandler: (HpEvents) -> Unit, + onContentSynchronized: () -> Unit, + onCacheLoaded: (Boolean) -> Unit, + ): Deferred { + trackEvent(TYPE_CACHE_LOAD) + return safeAsync(Dispatchers.IO) { + loadAndProcessCacheData(coroutineScope = this, eventHandler = eventHandler) { + cachedEntity -> + val cacheStatus = if (cachedEntity == null) VALUE_NOT_FOUND else VALUE_LOADED + trackEvent(TYPE_CACHE_PROCESS, mapOf(PARAM_CACHE_STATUS to cacheStatus)) + onCacheLoaded(cachedEntity != null) + if (cachedEntity != null) { + onContentSynchronized() + eventHandler(HpEvents.RenderedFirstTime) + } + } + } + } + + private suspend fun loadAndProcessCacheData( coroutineScope: CoroutineScope, eventHandler: (HpEvents) -> Unit, onCompletion: (NaviCacheEntity?) -> Unit, @@ -92,31 +213,34 @@ constructor( val cachedHomeContent = naviCacheRepository.get(NaviSharedDbKeys.HOME_TAB.name) if (cachedHomeContent == null) { - statusTrackEvent(CACHE_RETRIEVAL_NO_CONTENT) onCompletion(null) return } - statusTrackEvent(CACHE_CONTENT_PROCESSING_START) deserializer.setCacheEntity(cachedHomeContent.value) val priorityContentSection = deserializer.getPrioritySection() eventHandler(HpEvents.UpdatePrioritySectionData(priorityContentSection)) - statusTrackEvent(CACHE_PRIORITY_SECTION_EXTRACTED) + trackEvent( + TYPE_CACHE_PROCESS, + mapOf(PARAM_CONTENT_STATUS to VALUE_PRIORITY_SECTION_UPDATED), + ) val screenLayoutDeferred = coroutineScope.safeAsync { val screenLayout = deserializer.getScreenDefinitionWithoutContent() - statusTrackEvent(CACHE_SCREEN_LAYOUT_PROCESSED) screenLayout?.let { eventHandler(HpEvents.UpdateScreenWithoutContent(screenLayout)) + trackEvent( + TYPE_CACHE_PROCESS, + mapOf(PARAM_CONTENT_STATUS to VALUE_SCREEN_LAYOUT_UPDATED), + ) } } coroutineScope .safeAsync { val remainingContent = deserializer.getRemainingContent() - statusTrackEvent(CACHE_REMAINING_CONTENT_PROCESSED) remainingContent?.let { content -> screenLayoutDeferred.await() eventHandler( @@ -125,189 +249,165 @@ constructor( isRenderingFirstTime = true, ) ) + trackEvent( + TYPE_CACHE_PROCESS, + mapOf(PARAM_CONTENT_STATUS to VALUE_REMAINING_CONTENT_UPDATED), + ) } } .await() - statusTrackEvent(CACHE_CONTENT_PROCESSING_COMPLETED) onCompletion(cachedHomeContent) } - context(CoroutineScope) - fun synchronizeHomeContent( - isFirstTimeRender: Boolean, - fetchHomeContent: suspend () -> NaviCacheAltSourceEntity, - onContentSynchronized: () -> Unit, - eventHandler: (HpEvents) -> Unit, - ) { - statusTrackEventWithContext(SYNC_STARTED, isFirstTimeRender) - - if (isFirstTimeRender) { - handleFirstTimeRender(fetchHomeContent, onContentSynchronized, eventHandler) - } else { - handleSubsequentFetch(fetchHomeContent, onContentSynchronized, eventHandler) - } - } - - private fun CoroutineScope.handleFirstTimeRender( - fetchHomeContent: suspend () -> NaviCacheAltSourceEntity, - onContentSynchronized: () -> Unit, - eventHandler: (HpEvents) -> Unit, - ) { - var cacheDataLoaded = false - - val cacheLoadOperation = - launchCacheLoad( - eventHandler = eventHandler, - onContentSynchronized = onContentSynchronized, - onCacheLoaded = { cacheDataLoaded = it }, - ) - - launchNetworkFetchAndProcess( - fetchHomeContent = fetchHomeContent, - cacheLoadOperation = cacheLoadOperation, - cacheDataLoaded = cacheDataLoaded, - onContentSynchronized = onContentSynchronized, - eventHandler = eventHandler, - ) - } - - private fun CoroutineScope.handleSubsequentFetch( - fetchHomeContent: suspend () -> NaviCacheAltSourceEntity, - onContentSynchronized: () -> Unit, - eventHandler: (HpEvents) -> Unit, - ) { - safeLaunch(Dispatchers.IO) { - statusTrackEvent(NETWORK_SUBSEQUENT_FETCH_START) - val networkResponse = fetchNetworkData(fetchHomeContent) - - processNetworkResponse( - networkResponse = networkResponse, - contextType = ResponseContext.SUBSEQUENT_FETCH, - eventHandler = eventHandler, - onSuccess = { screenContent -> - screenContent?.let { - refreshScreenWithTracking( - screenContent = it, - eventHandler = eventHandler, - isFirstRender = false, - onContentSynchronized = onContentSynchronized, - ) - } - }, - ) - } - } - - private fun CoroutineScope.launchCacheLoad( - eventHandler: (HpEvents) -> Unit, - onContentSynchronized: () -> Unit, - onCacheLoaded: (Boolean) -> Unit, - ): Deferred = - safeAsync(Dispatchers.IO) { - statusTrackEvent(CACHE_DATA_RETRIEVAL_START) - retrieveHomeCacheData(coroutineScope = this, eventHandler = eventHandler) { cachedEntity - -> - if (cachedEntity == null) { - statusTrackEvent(CACHE_DATA_NOT_FOUND) - onCacheLoaded(false) - } else { - statusTrackEvent(CACHE_DATA_LOADED) - onCacheLoaded(true) - onContentSynchronized() - eventHandler(HpEvents.RenderedFirstTime) - } - } - } - - private fun CoroutineScope.launchNetworkFetchAndProcess( + private fun CoroutineScope.initiateNetworkSyncAndProcess( fetchHomeContent: suspend () -> NaviCacheAltSourceEntity, cacheLoadOperation: Deferred, cacheDataLoaded: Boolean, onContentSynchronized: () -> Unit, + onSelectiveRefreshStateChange: (SelectiveRefreshState) -> Unit, eventHandler: (HpEvents) -> Unit, + onHomeFetchSuccessFromNetwork: () -> Unit, ) { + trackEvent(TYPE_NETWORK_SYNC) safeLaunch(Dispatchers.IO) { - statusTrackEvent(NETWORK_FETCH_START) - val networkResponse = fetchNetworkData(fetchHomeContent) + val networkResponse = + fetchAndCompareAltSourceContent( + key = NaviSharedDbKeys.HOME_TAB.name, + getDataFromAltSource = fetchHomeContent, + ) - processNetworkResponse( + handleNetworkSyncResponse( networkResponse = networkResponse, - contextType = ResponseContext.FIRST_LAUNCH, eventHandler = eventHandler, - onSuccess = { parsedScreenContent -> + onNetworkError = { + trackEvent(TYPE_NETWORK_ERROR, mapOf(PARAM_ERROR_TYPE to VALUE_NETWORK_SYNC)) + onSelectiveRefreshStateChange(SelectiveRefreshState.ERROR) + }, + onScreenHashUnchanged = { + trackEvent(TYPE_NETWORK_SYNC, mapOf(PARAM_SYNC_STATUS to VALUE_HASH_UNCHANGED)) + onHomeFetchSuccessFromNetwork() + onSelectiveRefreshStateChange(SelectiveRefreshState.SUCCESS) + }, + onScreenContentUpdated = { screenContent, isDataUnchanged -> + trackEvent( + TYPE_NETWORK_SYNC, + mapOf( + PARAM_SYNC_STATUS to VALUE_CONTENT_UPDATED, + PARAM_IS_DATA_SAME to isDataUnchanged.toString(), + ), + ) cacheLoadOperation.await() - statusTrackEventWithContext(SYNC_CACHE_OPERATION_COMPLETED, cacheDataLoaded) - - parsedScreenContent?.let { - refreshScreenWithTracking( - screenContent = it, - eventHandler = eventHandler, - isFirstRender = !cacheDataLoaded, - onContentSynchronized = onContentSynchronized, - ) - if (!cacheDataLoaded) { - statusTrackEvent(SYNC_FIRST_RENDER_NETWORK) - eventHandler(HpEvents.RenderedFirstTime) - } + updateScreenContent( + screenContent = screenContent, + eventHandler = eventHandler, + isFirstRender = !cacheDataLoaded, + ) + if (!cacheDataLoaded && screenContent != null) { + eventHandler(HpEvents.RenderedFirstTime) } + onContentSynchronized() + if (!isDataUnchanged) { + onHomeFetchSuccessFromNetwork() + } + onSelectiveRefreshStateChange(SelectiveRefreshState.SUCCESS) }, ) } } - private suspend fun fetchNetworkData(fetchHomeContent: suspend () -> NaviCacheAltSourceEntity) = - naviCacheRepository.fetchFromAltSourceWithSimilarityCheck( - key = NaviSharedDbKeys.HOME_TAB.name, - version = BuildConfig.VERSION_CODE.toLong(), - getDataFromAltSource = fetchHomeContent, - ) + private suspend fun fetchAndCompareAltSourceContent( + key: String, + getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity, + ): HomeScreenContentResult { + trackEvent(TYPE_CONTENT_COMPARE, mapOf("key" to key)) - private suspend fun processNetworkResponse( - networkResponse: NaviCacheEntityInfo?, - contextType: ResponseContext, - eventHandler: (HpEvents) -> Unit, - onSuccess: suspend (AlchemistScreenDefinition?) -> Unit, - ) { - when { - networkResponse == null -> { - statusTrackEventWithContext(NETWORK_RESPONSE_NULL, contextType) + val entityFromAltSource = getDataFromAltSource.invoke() + val currentValueInDB = naviCacheRepository.get(key = key) + + return when { + !entityFromAltSource.isSuccess -> { + trackEvent(TYPE_NETWORK_ERROR, mapOf(PARAM_ERROR_TYPE to VALUE_ALT_SOURCE_FETCH)) + HomeScreenContentResult.Error(isCacheAvailable = currentValueInDB != null) } - networkResponse.isError -> { - statusTrackEventWithContext(NETWORK_ERROR, contextType) - if (contextType == ResponseContext.FIRST_LAUNCH) { - eventHandler(HpEvents.TriggerErrorState) - } + + entityFromAltSource.value.isNullOrEmpty() -> { + trackEvent(TYPE_CONTENT_COMPARE, mapOf(PARAM_CONTENT_STATUS to VALUE_EMPTY_CONTENT)) + HomeScreenContentResult.Success(isDataSame = true, isScreenHashSame = true) } + else -> { - statusTrackEventWithContext(NETWORK_SUCCESS, contextType) - val parsedContent = networkResponse.data?.let { deserializer.getScreen(it.value) } - onSuccess(parsedContent) + val naviCacheEntity = entityFromAltSource.toNaviCacheEntity(key) + val isDataSame = compareContentData(currentValueInDB, entityFromAltSource) + + trackEvent(TYPE_CONTENT_COMPARE, mapOf(PARAM_IS_DATA_SAME to isDataSame.toString())) + + HomeScreenContentResult.Success( + data = if (isDataSame) null else naviCacheEntity, + isDataSame = isDataSame, + isScreenHashSame = false, + ) + .also { + if (!isDataSame) { + trackEvent( + TYPE_CONTENT_COMPARE, + mapOf(PARAM_CONTENT_STATUS to VALUE_CONTENT_UPDATED), + ) + naviCacheRepository.save(naviCacheEntity) + } + } } } } - private fun refreshScreenWithTracking( - screenContent: AlchemistScreenDefinition, - eventHandler: (HpEvents) -> Unit, - isFirstRender: Boolean, - onContentSynchronized: () -> Unit, - ) { - statusTrackEventWithContext( - SYNC_REFRESH_CONTENT, - isFirstRender, - screenContent.screenStructure?.content?.widgets?.size ?: 0, - ) - refreshHomeScreenContent( - screenDefinition = screenContent, - eventHandler = eventHandler, - isRenderingFirstTime = isFirstRender, - ) - onContentSynchronized() + private fun compareContentData( + currentData: NaviCacheEntity?, + altData: NaviCacheAltSourceEntity?, + ): Boolean { + val currentDataValue = currentData?.value ?: return false + val altDataValue = altData?.value ?: return false + return currentData.version == altData.version && currentDataValue == altDataValue } - private enum class ResponseContext { - FIRST_LAUNCH, - SUBSEQUENT_FETCH, + private suspend fun handleNetworkSyncResponse( + networkResponse: HomeScreenContentResult, + eventHandler: (HpEvents) -> Unit, + onNetworkError: () -> Unit, + onScreenHashUnchanged: () -> Unit, + onScreenContentUpdated: + suspend (updatedContent: AlchemistScreenDefinition?, isDataUnchanged: Boolean) -> Unit, + ) { + when (networkResponse) { + is HomeScreenContentResult.Success -> { + if (networkResponse.isScreenHashSame) { + onScreenHashUnchanged() + } else { + val parsedContent = + networkResponse.data?.let { deserializer.getScreen(it.value) } + onScreenContentUpdated(parsedContent, networkResponse.isDataSame) + } + } + is HomeScreenContentResult.Error -> { + if (networkResponse.isCacheAvailable) { + onNetworkError() + } else { + eventHandler(HpEvents.TriggerErrorState) + } + } + } + } + + private fun updateScreenContent( + screenContent: AlchemistScreenDefinition?, + eventHandler: (HpEvents) -> Unit, + isFirstRender: Boolean, + ) { + if (screenContent == null) return + + eventHandler(HpEvents.UpdateScreenWithoutContent(screenContent)) + + screenContent.screenStructure?.content?.widgets?.let { homeWidgets -> + eventHandler(HpEvents.UpdateFrontLayerContent(homeWidgets, isFirstRender)) + } } } diff --git a/android/app/src/main/java/com/naviapp/home/viewmodel/HomeViewModel.kt b/android/app/src/main/java/com/naviapp/home/viewmodel/HomeViewModel.kt index 5d0887bd4a..132e92284a 100644 --- a/android/app/src/main/java/com/naviapp/home/viewmodel/HomeViewModel.kt +++ b/android/app/src/main/java/com/naviapp/home/viewmodel/HomeViewModel.kt @@ -194,16 +194,20 @@ constructor( handleDataModification() val screenHash = state.value.screenMetaData?.get(SCREEN_HASH) with(viewModelScope) { - homeContentProcessingUseCase.synchronizeHomeContent( + homeContentProcessingUseCase.syncHomeContentData( isFirstTimeRender = state.value.isRenderingFirstTime, - eventHandler = { sendEvent(it) }, - onContentSynchronized = { finishProcessing() }, + eventHandler = ::sendEvent, + onContentSynchronized = ::onContentSynchronized, fetchHomeContent = { fetchHomeDataFromApi( naeScreenName = naeScreenName, screenHash = screenHash, ) }, + onHomeFetchSuccessFromNetwork = ::onHomeFetchSuccessFromNetwork, + onSelectiveRefreshStateChange = { + selectiveRefreshHandler.updateRefreshState(this@HomeViewModel, it) + }, ) } } @@ -215,14 +219,13 @@ constructor( } } - private fun finishProcessing() { - setEffect { HpEffects.OnRenderActions } - updateHomeApiTimestamp() - selectiveRefreshHandler.handleSuccessState(this@HomeViewModel) + private fun onContentSynchronized() { + setEffect { HpEffects.OnPostRenderActions } + TemporaryStorageHelper.updateApiTs(TemporaryStorageHelper.HOME) } - private fun updateHomeApiTimestamp() { - TemporaryStorageHelper.updateApiTs(TemporaryStorageHelper.HOME) + private fun onHomeFetchSuccessFromNetwork() { + setEffect { HpEffects.OnApiSuccessRenderActions } naviAnalyticsEventTracker.onHomePageApiResponse( System.currentTimeMillis() - analyticsStartTs ) diff --git a/android/app/src/main/java/com/naviapp/utils/SelectiveRefreshHandler.kt b/android/app/src/main/java/com/naviapp/utils/SelectiveRefreshHandler.kt index eef8a32890..5a635a8dce 100644 --- a/android/app/src/main/java/com/naviapp/utils/SelectiveRefreshHandler.kt +++ b/android/app/src/main/java/com/naviapp/utils/SelectiveRefreshHandler.kt @@ -10,10 +10,31 @@ package com.naviapp.utils import com.navi.uitron.model.action.PublishEventAction import com.navi.uitron.model.action.PublishableEventData import com.navi.uitron.viewmodel.UiTronViewModel +import com.naviapp.home.model.SelectiveRefreshState +import com.naviapp.home.viewmodel.HomeViewModel import javax.inject.Inject class SelectiveRefreshHandler @Inject constructor() { + fun updateRefreshState( + homeViewModel: HomeViewModel, + selectiveRefreshState: SelectiveRefreshState, + ) { + when (selectiveRefreshState) { + SelectiveRefreshState.LOADING -> { + handleLoadingState(homeViewModel) + } + + SelectiveRefreshState.SUCCESS -> { + handleSuccessState(homeViewModel) + } + + SelectiveRefreshState.ERROR -> { + handleErrorState(homeViewModel) + } + } + } + fun handleSuccessState(viewModel: UiTronViewModel, stateKey: String? = null) { viewModel.handleAction( PublishEventAction( diff --git a/android/navi-base/src/main/java/com/navi/base/cache/model/NaviCacheEntityInfo.kt b/android/navi-base/src/main/java/com/navi/base/cache/model/NaviCacheEntityInfo.kt deleted file mode 100644 index fe34e32b12..0000000000 --- a/android/navi-base/src/main/java/com/navi/base/cache/model/NaviCacheEntityInfo.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * - * * Copyright © 2024-2025 by Navi Technologies Limited - * * All rights reserved. Strictly confidential - * - */ - -package com.navi.base.cache.model - -data class NaviCacheEntityInfo( - val isError: Boolean = false, - val data: NaviCacheEntity? = null, - val isComingFromAltSource: Boolean? = null, -) diff --git a/android/navi-base/src/main/java/com/navi/base/cache/repository/NaviCacheRepository.kt b/android/navi-base/src/main/java/com/navi/base/cache/repository/NaviCacheRepository.kt index b948643bb8..9a3538eac5 100644 --- a/android/navi-base/src/main/java/com/navi/base/cache/repository/NaviCacheRepository.kt +++ b/android/navi-base/src/main/java/com/navi/base/cache/repository/NaviCacheRepository.kt @@ -10,7 +10,6 @@ package com.navi.base.cache.repository import com.navi.base.cache.dao.NaviCacheDao import com.navi.base.cache.model.NaviCacheAltSourceEntity import com.navi.base.cache.model.NaviCacheEntity -import com.navi.base.cache.model.NaviCacheEntityInfo import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -54,20 +53,6 @@ interface NaviCacheRepository { emitOnFailure: Boolean = false, ): Flow - suspend fun fetchFromAltSourceWithSimilarityCheck( - key: String, - version: Long? = null, - getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity, - isCurrentAndAltDataSame: - (currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> Boolean = - fun(currentData, altData): Boolean { - val currentDataValue = currentData?.value ?: return false - val altDataValue = altData.value ?: return false - - return currentData.version == altData.version && currentDataValue == altDataValue - }, - ): NaviCacheEntityInfo? - suspend fun getLastUpdatedTime(key: String): Long } @@ -213,44 +198,6 @@ class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: Navi } } - override suspend fun fetchFromAltSourceWithSimilarityCheck( - key: String, - version: Long?, - getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity, - isCurrentAndAltDataSame: - (currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> Boolean, - ): NaviCacheEntityInfo? { - val currentValueInDB = get(key = key) - - val entityFromAltSource = getDataFromAltSource.invoke() - - if (entityFromAltSource.isSuccess.not() || entityFromAltSource.value.isNullOrEmpty()) { - return if (currentValueInDB == null) { - NaviCacheEntityInfo(isError = true) - } else { - null - } - } else { - val naviCacheEntity = - NaviCacheEntity( - key = key, - value = entityFromAltSource.value, - version = entityFromAltSource.version ?: 1, - ttl = entityFromAltSource.ttl, - clearOnLogout = entityFromAltSource.clearOnLogout, - metaData = entityFromAltSource.metaData, - ) - - val isDataSame = isCurrentAndAltDataSame(currentValueInDB, entityFromAltSource) - - if (isDataSame.not()) { - save(naviCacheEntity = naviCacheEntity) - return NaviCacheEntityInfo(data = naviCacheEntity, isComingFromAltSource = true) - } - return null - } - } - private suspend fun checkIfDBValueIsValidElseRemoveEntry( naviCacheEntity: NaviCacheEntity?, version: Long?,