From f6f898a7ac0aab1d4856a622afd2e63a3feb1c72 Mon Sep 17 00:00:00 2001 From: Naman Khurmi Date: Wed, 21 May 2025 21:54:30 +0530 Subject: [PATCH] NTP-66546 | Introduce minimal delay in the rendering process on the HomePage to reduce ANRs and ensure smooth click registration. (#16253) --- .../home/compose/activity/HomePageActivity.kt | 53 ++++++------- .../home/ui/content/FrontLayerContent.kt | 77 +++++++++++++++++-- .../com/naviapp/home/model/RenderingConfig.kt | 14 ++++ .../naviapp/home/viewmodel/HomeViewModel.kt | 43 +++++++++-- .../FirebaseRemoteConfigHelper.kt | 2 + 5 files changed, 144 insertions(+), 45 deletions(-) create mode 100644 android/app/src/main/java/com/naviapp/home/model/RenderingConfig.kt diff --git a/android/app/src/main/java/com/naviapp/home/compose/activity/HomePageActivity.kt b/android/app/src/main/java/com/naviapp/home/compose/activity/HomePageActivity.kt index 3f8075db31..1495a8e3f1 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/activity/HomePageActivity.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/activity/HomePageActivity.kt @@ -79,7 +79,6 @@ import com.navi.common.utils.getDeviceSignature import com.navi.common.utils.getLocalStorageLocation import com.navi.common.utils.getNetworkType import com.navi.common.utils.getScreenRefreshRate -import com.navi.common.utils.getTotalRamMemory import com.navi.common.utils.isInstalledInProfile import com.navi.common.utils.logNaviPayGlanceWidgetClick import com.navi.common.utils.observeNonNull @@ -197,6 +196,8 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch @AndroidEntryPoint @@ -325,6 +326,7 @@ class HomePageActivity : } } initObservers() + homeVM.fetchDelayedRenderingConfig(this@HomePageActivity) handleInitialTouchEventState() initErrors() fetchHomeData() @@ -341,41 +343,32 @@ class HomePageActivity : TraceManager.endAllTraces() } - private fun shouldDisableInitialTouchEvent(): Boolean { - val isTouchDisabled = - FirebaseRemoteConfigHelper.getBoolean( - FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT, - false, - ) - val maxRamThresholdGb = - FirebaseRemoteConfigHelper.getDouble( - FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT_MAX_RAM - ) - val deviceRamGb = getTotalRamMemory(this)?.toDoubleOrNull() ?: 0.0 - - return isTouchDisabled && deviceRamGb < maxRamThresholdGb - } - private fun handleInitialTouchEventState() { lifecycleScope.launch { - if (shouldDisableInitialTouchEvent()) { - homeVM.isClickEnabled.collect { isEnabled -> - when { - !isEnabled -> { - window.setFlags( - WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, - WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, - ) - naviAnalyticsEventTracker.homeClickDisabled() - } + combine(homeVM.delayedRenderingConfig, homeVM.isClickEnabled) { config, isEnabled -> + val isInitialTouchDisabled = config?.isInitialTouchDisabled.orFalse() + val hasLowerRamThanThreshold = config?.hasLowerRamThanThreshold.orFalse() + Pair(isInitialTouchDisabled && hasLowerRamThanThreshold, isEnabled) + } + .flowOn(Dispatchers.IO) + .collect { (isTouchDisabled, isEnabled) -> + if (isTouchDisabled) { + when { + !isEnabled -> { + window.setFlags( + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + ) + naviAnalyticsEventTracker.homeClickDisabled() + } - else -> { - window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) - naviAnalyticsEventTracker.homeClickEnabled() + else -> { + window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + naviAnalyticsEventTracker.homeClickEnabled() + } } } } - } } } diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/ui/content/FrontLayerContent.kt b/android/app/src/main/java/com/naviapp/home/compose/home/ui/content/FrontLayerContent.kt index ba786cd6a7..243458c9d0 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/home/ui/content/FrontLayerContent.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/home/ui/content/FrontLayerContent.kt @@ -33,10 +33,13 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -52,8 +55,10 @@ import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition +import com.navi.base.utils.orFalse import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition import com.navi.common.alchemist.model.WidgetRenderState +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.model.common.MastheadWidgetData import com.navi.naviwidgets.R import com.navi.pay.utils.conditional @@ -63,6 +68,9 @@ import com.naviapp.home.reducer.HpEvents import com.naviapp.home.utils.getHomeWidgetAnimationSpec import com.naviapp.home.viewmodel.HomeViewModel import com.naviapp.utils.Constants.HOME_SCREEN_IN_CAPS +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext @Composable @OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @@ -97,12 +105,21 @@ fun FrontLayerContent( TopNotchUI() } staticNudgeContainer() - RenderUiTronContent( - elementList = widgets, - homeWidgetRenderer = homeWidgetRenderer, - homeVM = homeVM, - onEvent = onEvent, - ) + if (isDelayedRenderingEnabled(homeVM)) { + ChunkedWidgetRenderer( + elementList = widgets, + homeWidgetRenderer = homeWidgetRenderer, + homeVM = homeVM, + onEvent = onEvent, + ) + } else { + RenderUiTronContent( + elementList = widgets, + homeWidgetRenderer = homeWidgetRenderer, + homeVM = homeVM, + onEvent = onEvent, + ) + } ContentLoaderLottie(isRenderingFirstTime) SideEffect { if (isRenderingFirstTime.not()) { @@ -118,6 +135,14 @@ fun FrontLayerContent( } } +@Composable +fun isDelayedRenderingEnabled(homeVM: () -> HomeViewModel): Boolean { + val renderingConfig by homeVM().delayedRenderingConfig.collectAsState() + val featureEnabled = renderingConfig?.isDelayedRenderingEnabled.orFalse() + val isRamBelowThreshold = renderingConfig?.hasLowerRamThanThreshold.orFalse() + return featureEnabled && isRamBelowThreshold +} + @Composable private fun MastheadWidget( homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, @@ -218,7 +243,43 @@ private fun AnimatedContainerForWidgets( } @Composable -internal fun RenderUiTronContent( +private fun ChunkedWidgetRenderer( + elementList: List>, + homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, + homeVM: () -> HomeViewModel, + onEvent: (event: HpEvents) -> Unit, + firstChunkSize: Int = 2, +) { + var displayedCount by remember { mutableIntStateOf(0) } + val delayBetweenChunks = remember { + FirebaseRemoteConfigHelper.getLong( + FirebaseRemoteConfigHelper.HOMEPAGE_WIDGET_RENDER_DELAY_MS, + 140L, + ) + } + + LaunchedEffect(elementList) { + withContext(Dispatchers.Default) { + displayedCount = displayedCount.coerceAtMost(elementList.size) + var nextChunk = if (displayedCount == 0) firstChunkSize else 1 + while (displayedCount < elementList.size) { + displayedCount = (displayedCount + nextChunk).coerceAtMost(elementList.size) + nextChunk += 1 + delay(delayBetweenChunks) + } + } + } + + RenderUiTronContent( + elementList = elementList.take(displayedCount), + homeWidgetRenderer = homeWidgetRenderer, + homeVM = homeVM, + onEvent = onEvent, + ) +} + +@Composable +private fun RenderUiTronContent( elementList: List>, homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, homeVM: () -> HomeViewModel, @@ -242,9 +303,11 @@ internal fun RenderUiTronContent( ) ) } + WidgetRenderState.NOT_VISIBLE -> { visible.value = false } + WidgetRenderState.VISIBLE -> { visible.value = true } diff --git a/android/app/src/main/java/com/naviapp/home/model/RenderingConfig.kt b/android/app/src/main/java/com/naviapp/home/model/RenderingConfig.kt new file mode 100644 index 0000000000..b0e0614676 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/model/RenderingConfig.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.home.model + +data class RenderingConfig( + val isInitialTouchDisabled: Boolean = false, + val isDelayedRenderingEnabled: Boolean = false, + val hasLowerRamThanThreshold: Boolean = false, +) 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 e67082167b..486d3ab026 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 @@ -7,6 +7,7 @@ package com.naviapp.home.viewmodel +import android.content.Context import androidx.lifecycle.viewModelScope import com.navi.analytics.utils.NaviTrackEvent import com.navi.base.cache.model.NaviCacheAltSourceEntity @@ -21,6 +22,7 @@ import com.navi.base.utils.ConnectivityObserver import com.navi.base.utils.isNotNullAndNotEmpty import com.navi.base.utils.orFalse import com.navi.common.basemvi.BaseMviViewModel +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper import com.navi.common.model.ModuleNameV2 import com.navi.common.model.NotificationSetting import com.navi.common.model.SettingsMedium @@ -29,6 +31,7 @@ import com.navi.common.uitron.helper.VideoViewHelper import com.navi.common.utils.Constants.SCREEN_HASH import com.navi.common.utils.Constants.UPI_NUX_SCREEN import com.navi.common.utils.TemporaryStorageHelper +import com.navi.common.utils.getTotalRamMemory import com.navi.naviwidgets.utils.CURRENT_VERSION_IN_STORE import com.navi.pay.common.model.view.NaviPayScreenType import com.navi.pay.utils.NAVI_PAY_CTA_URL_PREFIX @@ -42,6 +45,7 @@ import com.naviapp.home.common.handler.PostRenderTaskExecutor import com.naviapp.home.compose.activity.HomePageActivity import com.naviapp.home.compose.listener.HomeScreenCallbackListener import com.naviapp.home.model.BottomBarTabType +import com.naviapp.home.model.RenderingConfig import com.naviapp.home.reducer.HomeReducer import com.naviapp.home.reducer.HpEffects import com.naviapp.home.reducer.HpEvents @@ -62,11 +66,11 @@ import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout @@ -103,6 +107,35 @@ constructor( private val _internetConnectivity = MutableSharedFlow() val internetConnectivity: SharedFlow = _internetConnectivity + private val _delayedRenderingConfig: MutableStateFlow = MutableStateFlow(null) + val delayedRenderingConfig = _delayedRenderingConfig.asStateFlow() + + fun fetchDelayedRenderingConfig(context: Context) { + viewModelScope.launch(Dispatchers.IO) { + val isTouchDisabled = + FirebaseRemoteConfigHelper.getBoolean( + FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT, + false, + ) + val isDelayedRenderingEnabled = + FirebaseRemoteConfigHelper.getBoolean( + FirebaseRemoteConfigHelper.ENABLE_DELAYED_HP_WIDGETS_RENDERING, + false, + ) + val maxRamThreshold = + FirebaseRemoteConfigHelper.getDouble( + FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT_MAX_RAM + ) + val deviceRamGb = getTotalRamMemory(context)?.toDoubleOrNull() ?: 0.0 + _delayedRenderingConfig.value = + RenderingConfig( + isInitialTouchDisabled = isTouchDisabled, + isDelayedRenderingEnabled = isDelayedRenderingEnabled, + hasLowerRamThanThreshold = deviceRamGb < maxRamThreshold, + ) + } + } + init { viewModelScope.safeLaunch((Dispatchers.IO)) { observeActionCallback() } viewModelScope.safeLaunch((Dispatchers.IO)) { observeInternetConnectivity() } @@ -112,13 +145,7 @@ constructor( val isClickEnabled: StateFlow = _isClickEnabled fun updateClickEnabledState(isClickEnabled: Boolean) { - viewModelScope.safeLaunch(Dispatchers.IO) { - _isClickEnabled.value = isClickEnabled - delay(10_000) - if (_isClickEnabled.value.not()) { - updateClickEnabledState(true) - } - } + viewModelScope.safeLaunch(Dispatchers.IO) { _isClickEnabled.value = isClickEnabled } } private suspend fun observeActionCallback() { 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 523d8342b3..c58209ed76 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 @@ -317,6 +317,8 @@ object FirebaseRemoteConfigHelper { // HOME const val IS_GPS_POPUP_ENABLED = "GPS_POPUP_VISIBILITY" + const val ENABLE_DELAYED_HP_WIDGETS_RENDERING = "ENABLE_DELAYED_HP_WIDGETS_RENDERING" + const val HOMEPAGE_WIDGET_RENDER_DELAY_MS = "HOMEPAGE_WIDGET_RENDER_DELAY_MS" private fun getFirebaseRemoteConfig(): FirebaseRemoteConfig { val remoteConfig = Firebase.remoteConfig