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 cf079292ed..7d22adce89 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 @@ -180,9 +180,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 +import kotlinx.coroutines.withContext @AndroidEntryPoint class HomePageActivity : @@ -306,7 +305,6 @@ class HomePageActivity : } } initObservers() - homeVM.fetchDelayedRenderingConfig(this@HomePageActivity) handleInitialTouchEventState() initErrors() fetchHomeData() @@ -323,15 +321,10 @@ class HomePageActivity : } private fun handleInitialTouchEventState() { - lifecycleScope.launch { - 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) { + lifecycleScope.launch(Dispatchers.IO) { + homeVM.isClickEnabled.collect { isEnabled -> + if (homeVM.isInitialTouchDisabled()) { + withContext(Dispatchers.Main) { when { !isEnabled -> { window.setFlags( @@ -348,6 +341,7 @@ class HomePageActivity : } } } + } } } 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 9b58686c13..5d03ac6ce9 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 @@ -7,43 +7,24 @@ package com.naviapp.home.compose.home.ui.content -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable 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.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId @@ -53,23 +34,17 @@ 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 import com.navi.uitron.model.UiTronResponse +import com.naviapp.home.compose.home.ui.renderer.ChunkedWidgetRenderer +import com.naviapp.home.compose.home.ui.renderer.HomeWidgetRenderer +import com.naviapp.home.compose.home.ui.renderer.MastheadWidgetRenderer import com.naviapp.home.reducer.HpEffects import com.naviapp.home.reducer.HpEvents -import com.naviapp.home.utils.getHomeWidgetAnimationSpec -import com.naviapp.home.utils.shouldRenderWidget 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) @@ -77,7 +52,7 @@ fun FrontLayerContent( modifier: Modifier, frontLayerShape: Shape, widgets: List>?, - homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, + uitronRenderer: @Composable (UiTronResponse?) -> Unit, isRenderingFirstTime: Boolean, homeScrollState: () -> ScrollState, homeVM: () -> HomeViewModel, @@ -88,7 +63,7 @@ fun FrontLayerContent( isMastheadEnabled: Boolean, appBarHeight: Dp, ) { - CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + CompositionLocalProvider(LocalOverscrollFactory provides null) { Column( modifier .background(Color.White, frontLayerShape) @@ -98,35 +73,38 @@ fun FrontLayerContent( ) { if (!widgets.isNullOrEmpty()) { Column { - if (isMastheadEnabled) { - MastheadWidget(homeWidgetRenderer, mastheadWidget, appBarHeight) - } else { - TopNotchUI() - } + MastheadWidgetRenderer( + isMastheadEnabled = isMastheadEnabled, + uitronRenderer = uitronRenderer, + mastheadWidget = mastheadWidget, + appBarHeight = appBarHeight, + ) staticNudgeContainer() - if (isDelayedRenderingEnabled(homeVM)) { + if (isRenderingFirstTime && homeVM().isDelayedRenderingEnabled()) { ChunkedWidgetRenderer( elementList = widgets, - homeWidgetRenderer = homeWidgetRenderer, + homeWidgetRenderer = uitronRenderer, homeVM = homeVM, onEvent = onEvent, ) } else { - RenderUiTronContent( - elementList = widgets, - homeWidgetRenderer = homeWidgetRenderer, - homeVM = homeVM, - onEvent = onEvent, - ) + widgets.forEach { element -> + HomeWidgetRenderer( + element = element, + uitronRenderer = uitronRenderer, + homeVM = homeVM, + onEvent = onEvent, + ) + } } ContentLoaderLottie(isRenderingFirstTime) - SideEffect { - if (isRenderingFirstTime.not()) { - onEffect(HpEffects.HomePageUiRenderedEvent) - onEvent(HpEvents.UpdateIsHomePageRendered(true)) - } else { - onEffect(HpEffects.LogAppLaunchTime(HOME_SCREEN_IN_CAPS)) - } + } + SideEffect { + if (isRenderingFirstTime.not()) { + onEffect(HpEffects.HomePageUiRenderedEvent) + onEvent(HpEvents.UpdateIsHomePageRendered(true)) + } else { + onEffect(HpEffects.LogAppLaunchTime(HOME_SCREEN_IN_CAPS)) } } } else HomePageContentShimmer() @@ -134,205 +112,16 @@ 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, - mastheadWidget: MastheadWidgetData?, - appBarHeight: Dp, -) { - val shouldRender = - mastheadWidget?.let { shouldRenderWidget(it.renderActions?.preRenderAction) } ?: false - - if (!shouldRender) { - Box(modifier = Modifier.padding(top = appBarHeight, bottom = 16.dp).fillMaxWidth()) - return - } - mastheadWidget?.let { - Box( - modifier = - Modifier.height(IntrinsicSize.Max) - .padding(bottom = it.widgetBottomPadding.dp) - .fillMaxWidth() - ) { - Box(modifier = Modifier.matchParentSize()) { - homeWidgetRenderer(it.backgroundIllustration?.uiTronResponse) - } - Box(modifier = Modifier.padding(top = appBarHeight).fillMaxWidth()) { - homeWidgetRenderer(it.widgetData?.uiTronResponse) - } - } - } -} - -@Composable -fun TopNotchUI() { - Column( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box( - modifier = - Modifier.padding(vertical = 8.dp) - .width(40.dp) - .height(4.dp) - .background(color = Color(0xFFE3E5E5), shape = RoundedCornerShape(16.dp)) - ) - } -} - @Composable private fun ContentLoaderLottie(isPrioritySectionRendered: Boolean) { if (isPrioritySectionRendered) { + val composition by + rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.cta_loader_purple)) Row( modifier = Modifier.fillMaxWidth().padding(top = 32.dp, bottom = 100.dp), horizontalArrangement = Arrangement.Center, ) { - val composition by - rememberLottieComposition( - spec = LottieCompositionSpec.RawRes(R.raw.cta_loader_purple) - ) LottieAnimation(composition = composition, iterations = LottieConstants.IterateForever) } } } - -@Composable -private fun AnimatedContainerForWidgets( - isVisible: MutableState, - widget: AlchemistWidgetModelDefinition, - homeVM: HomeViewModel, - homeWidget: @Composable () -> Unit, -) { - val setImpressionTracker = - widget.widgetRenderActions?.onImpressionAction?.actions.isNullOrEmpty().not() - AnimatedVisibility( - visible = isVisible.value, - modifier = - Modifier.conditional(setImpressionTracker) { - onGloballyPositioned { layoutCoordinates -> - homeVM.sectionVisibilityTracker.updateSectionImpressionState( - widget, - layoutCoordinates, - ) { onImpressionAction -> - homeVM.handleActions(onImpressionAction) - } - } - }, - enter = - expandVertically( - expandFrom = Alignment.Top, - clip = true, - animationSpec = getHomeWidgetAnimationSpec(), - ) { - 0 - } + fadeIn(animationSpec = getHomeWidgetAnimationSpec()), - exit = - shrinkVertically( - shrinkTowards = Alignment.Bottom, - clip = true, - animationSpec = getHomeWidgetAnimationSpec(), - ) { - 0 - } + fadeOut(animationSpec = getHomeWidgetAnimationSpec()), - ) { - homeWidget() - } -} - -@Composable -private fun ChunkedWidgetRenderer( - elementList: List>, - homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, - homeVM: () -> HomeViewModel, - onEvent: (event: HpEvents) -> Unit, - firstChunkSize: Int = 2, -) { - if (elementList.isEmpty()) return - - val displayedCount = homeVM().widgetsDisplayed.coerceAtMost(elementList.size) - - val delayBetweenChunks = remember { - FirebaseRemoteConfigHelper.getLong( - FirebaseRemoteConfigHelper.HOMEPAGE_WIDGET_RENDER_DELAY_MS, - 140L, - ) - } - - LaunchedEffect(elementList) { - if (displayedCount < elementList.size) { - withContext(Dispatchers.Default) { - var nextChunk = if (displayedCount == 0) firstChunkSize else 1 - while (displayedCount < elementList.size) { - homeVM() - .updateDisplayedWidgetCount( - (displayedCount + nextChunk).coerceAtMost(elementList.size) - ) - nextChunk += 1 - delay(delayBetweenChunks) - } - } - } else { - homeVM().updateDisplayedWidgetCount(elementList.size) - } - } - - RenderUiTronContent( - elementList = elementList.take(displayedCount), - homeWidgetRenderer = homeWidgetRenderer, - homeVM = homeVM, - onEvent = onEvent, - ) -} - -@Composable -private fun RenderUiTronContent( - elementList: List>, - homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, - homeVM: () -> HomeViewModel, - onEvent: (event: HpEvents) -> Unit, -) { - elementList.forEach { element -> - element.widgetId?.let { widgetId -> - key(widgetId) { - val visible = remember { - mutableStateOf(element.widgetRenderState == WidgetRenderState.VISIBLE) - } - - LaunchedEffect(element.widgetRenderState) { - when (element.widgetRenderState) { - WidgetRenderState.NEWLY_ADDED -> { - visible.value = true - onEvent( - HpEvents.UpdateScreenContentWidgetRenderState( - widgetId, - WidgetRenderState.VISIBLE, - ) - ) - } - - WidgetRenderState.NOT_VISIBLE -> { - visible.value = false - } - - WidgetRenderState.VISIBLE -> { - visible.value = true - } - } - } - - AnimatedContainerForWidgets(visible, element, homeVM()) { - homeWidgetRenderer(element.widgetData) - } - } - } - } -} diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/ui/header/HomeTopBar.kt b/android/app/src/main/java/com/naviapp/home/compose/home/ui/header/HomeTopBar.kt index 2ad2db54df..afb2453d7e 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/home/ui/header/HomeTopBar.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/home/ui/header/HomeTopBar.kt @@ -44,7 +44,7 @@ fun HomeTopBar( appBarHeight: Dp, statusBarHeight: Dp, topBarContent: UiTronResponse?, - homeWidgetRenderer: @Composable (UiTronResponse) -> Unit, + uitronRenderer: @Composable (UiTronResponse) -> Unit, homeScrollState: () -> ScrollState, ) { val isScrollingUp by remember { derivedStateOf { homeScrollState().value.dp < appBarHeight } } @@ -60,7 +60,7 @@ fun HomeTopBar( } // Render the top bar - ToolbarRender(topBarModifier, statusBarHeight, topBarContent, homeWidgetRenderer) + ToolbarRender(topBarModifier, statusBarHeight, topBarContent, uitronRenderer) } @Composable @@ -103,7 +103,7 @@ fun MastheadTopBar( modifier = toolbarModifier, statusBarHeight = statusBarHeight, toolbarContent = toolbarConfig?.toolBarNav?.uiTronResponse, - homeWidgetRenderer = renderWidget, + uitronRenderer = renderWidget, mastheadEnable = true, ) } @@ -174,12 +174,12 @@ private fun ToolbarRender( modifier: Modifier, statusBarHeight: Dp, toolbarContent: UiTronResponse?, - homeWidgetRenderer: @Composable (UiTronResponse) -> Unit, + uitronRenderer: @Composable (UiTronResponse) -> Unit, mastheadEnable: Boolean = false, ) { Row(modifier = modifier, verticalAlignment = Alignment.Top) { Row(Modifier.padding(top = statusBarHeight)) { - toolbarContent?.let { homeWidgetRenderer(it) } ?: DefaultTopBar(mastheadEnable) + toolbarContent?.let { uitronRenderer(it) } ?: DefaultTopBar(mastheadEnable) } } } diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/ui/renderer/ChunkedWidgetRenderer.kt b/android/app/src/main/java/com/naviapp/home/compose/home/ui/renderer/ChunkedWidgetRenderer.kt new file mode 100644 index 0000000000..b0902f6791 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/compose/home/ui/renderer/ChunkedWidgetRenderer.kt @@ -0,0 +1,75 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.home.compose.home.ui.renderer + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.withFrameNanos +import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.uitron.model.UiTronResponse +import com.naviapp.home.reducer.HpEvents +import com.naviapp.home.viewmodel.HomeViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +@Composable +fun ChunkedWidgetRenderer( + elementList: List>?, + homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, + homeVM: () -> HomeViewModel, + onEvent: (event: HpEvents) -> Unit, + chunkSize: Int = 1, +) { + if (elementList.isNullOrEmpty()) return + + val delayBetweenWidgets = remember { + FirebaseRemoteConfigHelper.getLong( + FirebaseRemoteConfigHelper.HOMEPAGE_WIDGET_RENDER_DELAY_MS, + 140L, + ) + } + + val widgetsToShow = remember(elementList) { mutableIntStateOf(0) } + + Column { + elementList.take(widgetsToShow.intValue).forEach { element -> + key(element.widgetId) { + HomeWidgetRenderer( + element = element, + uitronRenderer = homeWidgetRenderer, + homeVM = homeVM, + onEvent = onEvent, + ) + } + } + } + + LaunchedEffect(elementList) { + widgetsToShow.intValue = 0 + elementList.indices + .chunked(chunkSize) + .asFlow() + .onEach { slice -> + val nextCount = slice.last() + 1 + widgetsToShow.intValue = nextCount + withFrameNanos {} + if (slice.last() < elementList.lastIndex) { + delay(delayBetweenWidgets) + } + } + .collect() + onEvent(HpEvents.RenderedFirstTime) + } +} diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/ui/renderer/HomeWidgetRenderer.kt b/android/app/src/main/java/com/naviapp/home/compose/home/ui/renderer/HomeWidgetRenderer.kt new file mode 100644 index 0000000000..afc11b5568 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/compose/home/ui/renderer/HomeWidgetRenderer.kt @@ -0,0 +1,142 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.home.compose.home.ui.renderer + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition +import com.navi.common.alchemist.model.WidgetRenderState +import com.navi.pay.utils.conditional +import com.navi.uitron.model.UiTronResponse +import com.naviapp.home.reducer.HpEvents +import com.naviapp.home.utils.getHomeWidgetAnimationSpec +import com.naviapp.home.viewmodel.HomeViewModel + +@Composable +fun HomeWidgetRenderer( + element: AlchemistWidgetModelDefinition, + uitronRenderer: @Composable (UiTronResponse?) -> Unit, + homeVM: () -> HomeViewModel, + onEvent: (event: HpEvents) -> Unit, + onRenderComplete: () -> Unit = {}, +) { + val widgetId = element.widgetId + if (widgetId == null) { + onRenderComplete() + return + } + val visible = remember { + mutableStateOf(element.widgetRenderState == WidgetRenderState.VISIBLE) + } + key(widgetId) { + HandleWidgetVisibility( + widgetId = widgetId, + elementState = element.widgetRenderState, + onVisibilityChange = { visible.value = it }, + onEvent = onEvent, + ) + AnimatedContainerForWidgets( + isVisible = visible, + widget = element, + homeVM = homeVM, + homeWidget = { uitronRenderer(element.widgetData) }, + ) + SideEffect { onRenderComplete() } + } +} + +@Composable +private fun HandleWidgetVisibility( + widgetId: String, + elementState: WidgetRenderState, + onVisibilityChange: (Boolean) -> Unit, + onEvent: (event: HpEvents) -> Unit, +) { + LaunchedEffect(elementState) { + when (elementState) { + WidgetRenderState.NEWLY_ADDED -> { + onVisibilityChange(true) + onEvent( + HpEvents.UpdateScreenContentWidgetRenderState( + widgetId, + WidgetRenderState.VISIBLE, + ) + ) + } + + WidgetRenderState.NOT_VISIBLE -> onVisibilityChange(false) + + WidgetRenderState.VISIBLE -> onVisibilityChange(true) + } + } +} + +@Composable +private fun AnimatedContainerForWidgets( + isVisible: MutableState, + widget: AlchemistWidgetModelDefinition, + homeVM: () -> HomeViewModel, + homeWidget: @Composable () -> Unit, +) { + val setImpressionTracker = + widget.widgetRenderActions?.onImpressionAction?.actions.isNullOrEmpty().not() + AnimatedVisibility( + visible = isVisible.value, + modifier = + Modifier.conditional(setImpressionTracker) { + onGloballyPositioned { layoutCoordinates -> + homeVM().sectionVisibilityTracker.updateSectionImpressionState( + widget, + layoutCoordinates, + ) { onImpressionAction -> + homeVM().handleActions(onImpressionAction) + } + } + }, + enter = enterTransitionForAnimatedContainer(), + exit = exitTransitionForAnimatedContainer(), + content = { homeWidget() }, + ) +} + +@Composable +private fun exitTransitionForAnimatedContainer(): ExitTransition { + return shrinkVertically( + shrinkTowards = Alignment.Bottom, + clip = true, + animationSpec = getHomeWidgetAnimationSpec(), + ) { + 0 + } + fadeOut(animationSpec = getHomeWidgetAnimationSpec()) +} + +@Composable +private fun enterTransitionForAnimatedContainer(): EnterTransition { + return expandVertically( + expandFrom = Alignment.Top, + clip = true, + animationSpec = getHomeWidgetAnimationSpec(), + ) { + 0 + } + fadeIn(animationSpec = getHomeWidgetAnimationSpec()) +} diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/ui/renderer/MastheadWidgetRenderer.kt b/android/app/src/main/java/com/naviapp/home/compose/home/ui/renderer/MastheadWidgetRenderer.kt new file mode 100644 index 0000000000..f9b82f351e --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/compose/home/ui/renderer/MastheadWidgetRenderer.kt @@ -0,0 +1,90 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.home.compose.home.ui.renderer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.navi.common.model.common.MastheadWidgetData +import com.navi.uitron.model.UiTronResponse +import com.naviapp.home.utils.shouldRenderWidget + +@Composable +fun MastheadWidgetRenderer( + isMastheadEnabled: Boolean, + uitronRenderer: @Composable (UiTronResponse?) -> Unit, + mastheadWidget: MastheadWidgetData?, + appBarHeight: Dp, +) { + if (isMastheadEnabled) { + MastheadWidget(uitronRenderer, mastheadWidget, appBarHeight) + } else { + TopNotchUI() + } +} + +@Composable +private fun MastheadWidget( + homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, + mastheadWidget: MastheadWidgetData?, + appBarHeight: Dp, +) { + val shouldRender = + mastheadWidget?.let { shouldRenderWidget(it.renderActions?.preRenderAction) } == true + + if (!shouldRender) { + Box(modifier = Modifier.padding(top = appBarHeight, bottom = 16.dp).fillMaxWidth()) + return + } + mastheadWidget.let { + Box( + modifier = + Modifier.height(IntrinsicSize.Max) + .padding(bottom = it.widgetBottomPadding.dp) + .fillMaxWidth() + ) { + Box(modifier = Modifier.matchParentSize()) { + homeWidgetRenderer(it.backgroundIllustration?.uiTronResponse) + } + Box(modifier = Modifier.padding(top = appBarHeight).fillMaxWidth()) { + homeWidgetRenderer(it.widgetData?.uiTronResponse) + } + } + } +} + +@Composable +private fun TopNotchUI() { + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = + Modifier.padding(vertical = 8.dp) + .width(40.dp) + .height(4.dp) + .background(color = Color(0xFFE3E5E5), shape = RoundedCornerShape(16.dp)) + ) + } +} diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreen.kt b/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreen.kt index 695ab0d659..d8638326b9 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreen.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreen.kt @@ -26,7 +26,7 @@ import com.naviapp.home.compose.activity.HomePageActivity import com.naviapp.home.compose.home.ui.footer.utils.handleBottomBarData import com.naviapp.home.compose.home.utils.InitHomeScreenComponents import com.naviapp.home.compose.home.utils.retryHomePageApi -import com.naviapp.home.compose.widgetfactory.HomeWidgetRenderer +import com.naviapp.home.compose.widgetfactory.HomeUitronRenderer import com.naviapp.home.reducer.HpEvents import com.naviapp.home.reducer.HpStates import com.naviapp.home.utils.WidgetRenderer @@ -99,8 +99,8 @@ fun HomeScreen( true -> { if (hpStates().isLoading) { HomeScreenScaffoldRoot( - homeWidgetRenderer = { - HomeWidgetRenderer( + uitronRenderer = { + HomeUitronRenderer( widgetData = it?.data, viewModel = homeVM(), composeView = it?.parentComposeView, @@ -135,8 +135,8 @@ fun HomeScreen( ) } else { HomeScreenScaffoldRoot( - homeWidgetRenderer = { widget -> - HomeWidgetRenderer( + uitronRenderer = { widget -> + HomeUitronRenderer( widgetData = widget?.data, viewModel = homeVM(), composeView = widget?.parentComposeView, diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreenScaffold.kt b/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreenScaffold.kt index 0156fd2d57..861f6d8792 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreenScaffold.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreenScaffold.kt @@ -56,7 +56,7 @@ internal fun HomeScreenScaffoldRoot( hpStates: () -> HpStates, homeScrollState: () -> ScrollState, homeVM: () -> HomeViewModel, - homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, + uitronRenderer: @Composable (UiTronResponse?) -> Unit, staticNudgeContainer: @Composable () -> Unit, uiController: SystemUiController, ) { @@ -75,7 +75,7 @@ internal fun HomeScreenScaffoldRoot( frontLayerShape = frontLayerShape, homeScrollState = homeScrollState, homeVM = homeVM, - homeWidgetRenderer = homeWidgetRenderer, + uitronRenderer = uitronRenderer, onEffect = { homeVM().setEffect { it } }, onEvent = { homeVM().sendEvent(it) }, isRenderingFirstTime = hpStates().isRenderingFirstTime, @@ -88,7 +88,7 @@ internal fun HomeScreenScaffoldRoot( modifier = Modifier, statusBarHeight = statusBarHeight, toolbarConfig = (hpStates().collapsingToolbar), - renderWidget = homeWidgetRenderer, + renderWidget = uitronRenderer, scrollStateProvider = homeScrollState, onEffect = { homeVM().setEffect { it } }, uiController = uiController, @@ -96,14 +96,14 @@ internal fun HomeScreenScaffoldRoot( } else { BackLayerContent( backLayerHeight = backLayerHeight, - homeWidgetRenderer = homeWidgetRenderer, + homeWidgetRenderer = uitronRenderer, backLayerData = (hpStates().collapsingToolbar?.collapsingTopNav)?.uiTronResponse, ) FrontLayerWithBackDropScroll( hpStates = hpStates, homeScrollState = homeScrollState, homeVM = homeVM, - homeWidgetRenderer = homeWidgetRenderer, + uitronRenderer = uitronRenderer, frontLayerShape = frontLayerShape, appBarHeight = appBarHeight, backLayerHeight = backLayerHeight, @@ -114,7 +114,7 @@ internal fun HomeScreenScaffoldRoot( appBarHeight = appBarHeight, statusBarHeight = statusBarHeight, topBarContent = (hpStates().collapsingToolbar?.toolBarNav)?.uiTronResponse, - homeWidgetRenderer = homeWidgetRenderer, + uitronRenderer = uitronRenderer, homeScrollState = homeScrollState, ) } @@ -139,7 +139,7 @@ private fun FrontLayerWithBackDropScroll( hpStates: () -> HpStates, homeScrollState: () -> ScrollState, homeVM: () -> HomeViewModel, - homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit, + uitronRenderer: @Composable (UiTronResponse?) -> Unit, frontLayerShape: RoundedCornerShape, appBarHeight: Dp, backLayerHeight: Dp, @@ -178,7 +178,7 @@ private fun FrontLayerWithBackDropScroll( .padding(bottom = appBarHeight), frontLayerShape = frontLayerShape, widgets = (hpStates().frontLayerContent), - homeWidgetRenderer = homeWidgetRenderer, + uitronRenderer = uitronRenderer, isRenderingFirstTime = hpStates().isRenderingFirstTime, homeScrollState = homeScrollState, homeVM = homeVM, diff --git a/android/app/src/main/java/com/naviapp/home/compose/widgetfactory/WidgetRenderer.kt b/android/app/src/main/java/com/naviapp/home/compose/widgetfactory/WidgetRenderer.kt index d4c3d6df24..fdee51544e 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/widgetfactory/WidgetRenderer.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/widgetfactory/WidgetRenderer.kt @@ -16,7 +16,7 @@ import com.naviapp.home.compose.uiTron.renderer.HomeCustomUiTronRenderer import com.naviapp.home.viewmodel.HomeViewModel @Composable -fun HomeWidgetRenderer( +fun HomeUitronRenderer( widgetData: MutableMap?, composeView: List?, viewModel: HomeViewModel, 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 deleted file mode 100644 index b0e0614676..0000000000 --- a/android/app/src/main/java/com/naviapp/home/model/RenderingConfig.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * - * * 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/reducer/HomeReducer.kt b/android/app/src/main/java/com/naviapp/home/reducer/HomeReducer.kt index 69bd6a6e46..dcb3a6a4d3 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 @@ -85,9 +85,10 @@ class HomeReducer : BaseReducer { previousState.copy(frontLayerContent = newContent) } is HpEvents.UpdateFrontLayerContent -> { + val newFirst = previousState.isRenderingFirstTime && event.isRenderingFirstTime val updatedList = updateScreenContent( - renderingFirstTime = event.isRenderingFirstTime, + renderingFirstTime = newFirst, newWidgets = event.content, oldWidgets = previousState.frontLayerContent, ) @@ -95,6 +96,7 @@ class HomeReducer : BaseReducer { isLoading = false, isError = false, frontLayerContent = updatedList, + isRenderingFirstTime = newFirst, ) } is HpEvents.UpdatePrioritySectionData -> { 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 c536dc990e..fbc2ccdbc2 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 @@ -68,6 +68,9 @@ constructor( 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" + + // Lower ram device + private const val VALUE_SCREEN_UPDATED = "screen_updated_on_low_end_device" } private fun trackEvent(type: String, params: Map = emptyMap()) { @@ -77,9 +80,10 @@ constructor( NaviTrackEvent.trackEvent(EVENT_NAME, eventParams) } - context(CoroutineScope) fun syncHomeContentData( + scope: CoroutineScope, isFirstTimeRender: Boolean, + isDelayedRenderingEnabled: Boolean, fetchHomeContent: suspend (shouldShowNae: Boolean) -> NaviCacheAltSourceEntity, onContentSynchronized: () -> Unit, onHomeFetchSuccessFromNetwork: () -> Unit, @@ -88,7 +92,8 @@ constructor( ) { trackEvent(TYPE_SYNC_START, mapOf(PARAM_IS_FIRST_TIME to isFirstTimeRender.toString())) if (isFirstTimeRender) { - initializeFirstTimeContentSync( + scope.initializeFirstTimeContentSync( + isDelayedRenderingEnabled, fetchHomeContent, onContentSynchronized, eventHandler, @@ -96,7 +101,7 @@ constructor( onHomeFetchSuccessFromNetwork, ) } else { - performIncrementalContentSync( + scope.performIncrementalContentSync( { fetchHomeContent(false) }, onContentSynchronized, eventHandler, @@ -107,6 +112,7 @@ constructor( } private fun CoroutineScope.initializeFirstTimeContentSync( + isDelayedRenderingEnabled: Boolean, fetchHomeContent: suspend (shouldShowNae: Boolean) -> NaviCacheAltSourceEntity, onContentSynchronized: () -> Unit, eventHandler: (HpEvents) -> Unit, @@ -118,6 +124,7 @@ constructor( val cacheLoadOperation = initializeCacheDataLoad( + isDelayedRenderingEnabled = isDelayedRenderingEnabled, eventHandler = eventHandler, onContentSynchronized = onContentSynchronized, onCacheLoaded = { cacheDataLoaded.value = it }, @@ -190,29 +197,37 @@ constructor( } private fun CoroutineScope.initializeCacheDataLoad( + isDelayedRenderingEnabled: Boolean, 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) - } - } + loadAndProcessCacheData( + isDelayedRenderingEnabled = isDelayedRenderingEnabled, + eventHandler = eventHandler, + onContentSynchronized = onContentSynchronized, + onCompletion = { 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) + } + }, + onCacheLoaded = onCacheLoaded, + ) } } - private suspend fun loadAndProcessCacheData( - coroutineScope: CoroutineScope, + private suspend fun CoroutineScope.loadAndProcessCacheData( + isDelayedRenderingEnabled: Boolean, eventHandler: (HpEvents) -> Unit, + onContentSynchronized: () -> Unit, onCompletion: (NaviCacheEntity?) -> Unit, + onCacheLoaded: (Boolean) -> Unit, ) { val cachedHomeContent = naviCacheRepository.get(NaviSharedDbKeys.HOME_TAB.name) @@ -223,22 +238,30 @@ constructor( deserializer.setCacheEntity(cachedHomeContent.value) - val prioritySection = deserializer.getPrioritySection() - - val updatedPriorityContentSection = - HomePrioritySectionData( - content = getFilteredWidgets(prioritySection.content), - topNav = prioritySection.topNav, + if (isDelayedRenderingEnabled) { + val content = deserializer.getScreen(cachedHomeContent.value) + updateScreenContent( + screenContent = content, + eventHandler = eventHandler, + isFirstRender = true, + ) + trackEvent(TYPE_CACHE_PROCESS, mapOf(PARAM_CONTENT_STATUS to VALUE_SCREEN_UPDATED)) + onCacheLoaded(true) + onContentSynchronized() + } else { + val prioritySection = deserializer.getPrioritySection() + val updatedPriorityContentSection = + HomePrioritySectionData( + content = getFilteredWidgets(prioritySection.content), + topNav = prioritySection.topNav, + ) + eventHandler(HpEvents.UpdatePrioritySectionData(updatedPriorityContentSection)) + trackEvent( + TYPE_CACHE_PROCESS, + mapOf(PARAM_CONTENT_STATUS to VALUE_PRIORITY_SECTION_UPDATED), ) - eventHandler(HpEvents.UpdatePrioritySectionData(updatedPriorityContentSection)) - trackEvent( - TYPE_CACHE_PROCESS, - mapOf(PARAM_CONTENT_STATUS to VALUE_PRIORITY_SECTION_UPDATED), - ) - - val screenLayoutDeferred = - coroutineScope.safeAsync { + val screenLayoutDeferred = safeAsync { val screenLayout = deserializer.getScreenDefinitionWithoutContent() screenLayout?.let { eventHandler(HpEvents.UpdateScreenWithoutContent(screenLayout)) @@ -249,26 +272,25 @@ constructor( } } - coroutineScope - .safeAsync { - val remainingContent = deserializer.getRemainingContent() - remainingContent?.let { content -> - screenLayoutDeferred.await() - eventHandler( - HpEvents.UpdateFrontLayerContent( - content = getFilteredWidgets(content) ?: emptyList(), - isRenderingFirstTime = true, + safeAsync { + val remainingContent = deserializer.getRemainingContent() + remainingContent?.let { content -> + screenLayoutDeferred.await() + eventHandler( + HpEvents.UpdateFrontLayerContent( + content = getFilteredWidgets(content) ?: emptyList(), + isRenderingFirstTime = true, + ) ) - ) - trackEvent( - TYPE_CACHE_PROCESS, - mapOf(PARAM_CONTENT_STATUS to VALUE_REMAINING_CONTENT_UPDATED), - ) + trackEvent( + TYPE_CACHE_PROCESS, + mapOf(PARAM_CONTENT_STATUS to VALUE_REMAINING_CONTENT_UPDATED), + ) + } } - } - .await() - - onCompletion(cachedHomeContent) + .await() + onCompletion(cachedHomeContent) + } } private fun CoroutineScope.initiateNetworkSyncAndProcess( diff --git a/android/app/src/main/java/com/naviapp/home/usecase/RenderingConfigurationUseCase.kt b/android/app/src/main/java/com/naviapp/home/usecase/RenderingConfigurationUseCase.kt new file mode 100644 index 0000000000..9789ddb529 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/usecase/RenderingConfigurationUseCase.kt @@ -0,0 +1,85 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.home.usecase + +import android.content.Context +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.common.utils.getTotalRamMemory +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +/** + * Use case responsible for managing rendering configuration decisions based on device capabilities + * and remote configuration flags. This helps optimize the app's rendering behavior based on device + * constraints. + */ +class RenderingConfigurationUseCase +@Inject +constructor(@ApplicationContext private val context: Context) { + companion object { + private const val DEFAULT_TOUCH_DISABLED = false + private const val DEFAULT_DELAYED_RENDERING = false + } + + /** + * The device RAM in GB, calculated once and cached for reuse. Returns 0.0 if RAM information + * cannot be determined. + */ + val deviceRamGb: Double by lazy { getTotalRamMemory(context)?.toDoubleOrNull() ?: 0.0 } + + /** Flag indicating if the device has lower RAM than the threshold defined in remote config. */ + private val isRamBelowThreshold by lazy { + val maxRamThreshold = + FirebaseRemoteConfigHelper.getDouble( + FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT_MAX_RAM + ) + deviceRamGb < maxRamThreshold + } + + /** Flag indicating if delayed rendering is enabled in Firebase Remote Config. */ + private val isDelayedRenderingFeatureEnabled by lazy { + FirebaseRemoteConfigHelper.getBoolean( + FirebaseRemoteConfigHelper.ENABLE_DELAYED_HP_WIDGETS_RENDERING, + DEFAULT_DELAYED_RENDERING, + ) + } + + /** Flag indicating if disabling initial touch events is enabled in Firebase Remote Config. */ + private val isTouchDisabledFeatureEnabled by lazy { + FirebaseRemoteConfigHelper.getBoolean( + FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT, + DEFAULT_TOUCH_DISABLED, + ) + } + + /** Cached result for delayed rendering decision. */ + private val _isDelayedRenderingEnabled by lazy { + isDelayedRenderingFeatureEnabled && isRamBelowThreshold + } + + /** Cached result for initial touch disabled decision. */ + private val _isInitialTouchDisabled by lazy { + isTouchDisabledFeatureEnabled && isRamBelowThreshold + } + + /** + * Returns whether delayed rendering should be enabled based on device capabilities and remote + * configuration. + * + * @return true if delayed rendering should be enabled, false otherwise + */ + fun isDelayedRenderingEnabled() = _isDelayedRenderingEnabled + + /** + * Returns whether initial touch events should be disabled based on device capabilities and + * remote configuration. + * + * @return true if initial touch events should be disabled, false otherwise + */ + fun isInitialTouchDisabled() = _isInitialTouchDisabled +} 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 8ee0208ace..fc42882f66 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 @@ -9,7 +9,6 @@ package com.naviapp.home.viewmodel import android.content.Context import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.viewModelScope @@ -26,7 +25,6 @@ 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 @@ -35,7 +33,6 @@ 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 @@ -49,7 +46,6 @@ 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 @@ -59,6 +55,7 @@ import com.naviapp.home.usecase.FetchHomeItemsUseCase import com.naviapp.home.usecase.HandleCtaUseCase import com.naviapp.home.usecase.HandleUpiUseCase import com.naviapp.home.usecase.HomeContentProcessingUseCase +import com.naviapp.home.usecase.RenderingConfigurationUseCase import com.naviapp.nux.handler.NewUserExperienceHandler import com.naviapp.utils.Constants.EmailConstants.IS_USER_EMAIL_SUBMITTED import com.naviapp.utils.Constants.HOME_SCREEN_SCREEN_HASH @@ -75,7 +72,6 @@ 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 @@ -96,6 +92,7 @@ constructor( val sectionVisibilityTracker: HomePageSectionImpressionTracker, val postRenderTaskExecutor: PostRenderTaskExecutor, private val homeContentProcessingUseCase: HomeContentProcessingUseCase, + private val renderingConfigurationUseCase: RenderingConfigurationUseCase, @ApplicationContext val context: Context, ) : BaseMviViewModel( @@ -108,53 +105,19 @@ constructor( private var analyticsStartTs = System.currentTimeMillis() private var isHomeUIRenderedEventLogged = false var isErrorNaeTriggered = false - private val totalRamMemory = getTotalRamMemory(context = context)?.toDoubleOrNull() ?: 100.0 + private var postApiOneTimeActionTriggered by mutableStateOf(false) // Internet Connectivity private val _internetConnectivity = MutableSharedFlow() val internetConnectivity: SharedFlow = _internetConnectivity - var widgetsDisplayed by mutableIntStateOf(0) - private set + fun isDelayedRenderingEnabled() = renderingConfigurationUseCase.isDelayedRenderingEnabled() - private var postApiOneTimeActionTriggered by mutableStateOf(false) - - fun updateDisplayedWidgetCount(count: Int) { - widgetsDisplayed = count - } - - 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, - ) - } - } + fun isInitialTouchDisabled() = renderingConfigurationUseCase.isInitialTouchDisabled() init { - viewModelScope.safeLaunch((Dispatchers.IO)) { observeActionCallback() } - viewModelScope.safeLaunch((Dispatchers.IO)) { observeInternetConnectivity() } + viewModelScope.safeLaunch(Dispatchers.IO) { observeActionCallback() } + viewModelScope.safeLaunch(Dispatchers.IO) { observeInternetConnectivity() } } private val _isClickEnabled: MutableStateFlow = MutableStateFlow(false) @@ -207,7 +170,7 @@ constructor( screenHash = screenHash, shouldShowNae = shouldShowNae.orFalse(), naeScreenName = naeScreenName, - totalRam = totalRamMemory, + totalRam = renderingConfigurationUseCase.deviceRamGb, onFailure = { errorMessage, errors -> setEffect { HpEffects.OnApiFailure(errorMessage, errors) } }, @@ -273,24 +236,24 @@ constructor( screenHash.orEmpty(), ) } - with(viewModelScope) { - homeContentProcessingUseCase.syncHomeContentData( - isFirstTimeRender = state.value.isRenderingFirstTime, - eventHandler = ::sendEvent, - onContentSynchronized = ::onContentSynchronized, - fetchHomeContent = { - fetchHomeDataFromApi( - naeScreenName = naeScreenName, - screenHash = screenHash, - shouldShowNae = it, - ) - }, - onHomeFetchSuccessFromNetwork = ::onHomeFetchSuccessFromNetwork, - onSelectiveRefreshStateChange = { - selectiveRefreshHandler.updateRefreshState(this@HomeViewModel, it) - }, - ) - } + homeContentProcessingUseCase.syncHomeContentData( + scope = this.viewModelScope, + isFirstTimeRender = state.value.isRenderingFirstTime, + isDelayedRenderingEnabled = isDelayedRenderingEnabled(), + eventHandler = ::sendEvent, + onContentSynchronized = ::onContentSynchronized, + fetchHomeContent = { + fetchHomeDataFromApi( + naeScreenName = naeScreenName, + screenHash = screenHash, + shouldShowNae = it, + ) + }, + onHomeFetchSuccessFromNetwork = ::onHomeFetchSuccessFromNetwork, + onSelectiveRefreshStateChange = { + selectiveRefreshHandler.updateRefreshState(this@HomeViewModel, it) + }, + ) } } diff --git a/android/app/src/main/java/com/naviapp/screenOverlay/scratchcard/ui/ScratchCardOverlayRenderer.kt b/android/app/src/main/java/com/naviapp/screenOverlay/scratchcard/ui/ScratchCardOverlayRenderer.kt index c5788cd1f6..0c2659df5b 100644 --- a/android/app/src/main/java/com/naviapp/screenOverlay/scratchcard/ui/ScratchCardOverlayRenderer.kt +++ b/android/app/src/main/java/com/naviapp/screenOverlay/scratchcard/ui/ScratchCardOverlayRenderer.kt @@ -41,7 +41,6 @@ import com.navi.rr.scratchcard.ui.compose.ScratchCardRendererV2 import com.navi.rr.utils.constants.ScratchCardAnimationConstants.SCRATCH_CARD_DEFAULT_POSITION import com.navi.rr.utils.constants.ScratchCardAnimationConstants.SCRATCH_CARD_INITIAL_POSITION import com.navi.rr.utils.ext.clickable -import com.naviapp.home.compose.home.ui.content.isDelayedRenderingEnabled import com.naviapp.home.model.BottomBarTabType import com.naviapp.home.reducer.HpStates import com.naviapp.home.viewmodel.HomeViewModel @@ -75,7 +74,7 @@ fun ScratchCardOverlayRenderer( ), label = EMPTY, ) - val delayedRenderingEnabled = isDelayedRenderingEnabled(homeVM) + val delayedRenderingEnabled = homeVM().isDelayedRenderingEnabled() var isReadyToRender by remember { mutableStateOf(!delayedRenderingEnabled) } if ( shouldShowScratchCard(