From fb59174d6edfa1a5456a4e3818b6a310e56f10f9 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Sat, 28 Jun 2025 00:30:07 +0530 Subject: [PATCH 1/3] Migrated the `DonationLayout` to Jetpack Compose. * Implemented the `DonationLayout` using Compose UI. * Refactored the `ReaderScreen` to integrate the new Compose-based layout. * Added a maximum width constraint to the donation layout to prevent it from appearing too wide in landscape mode or on tablets. * Removed unused code from the project. --- app/src/main/res/values/dimens.xml | 1 - .../core/main/reader/CoreReaderFragment.kt | 103 ++--------- .../core/main/reader/DonationLayout.kt | 173 ++++++++++++++++++ .../core/main/reader/ReaderScreen.kt | 41 ++++- .../core/main/reader/ReaderScreenState.kt | 14 +- .../kiwixmobile/core/utils/ComposeDimens.kt | 2 + .../layout/layout_donation_bottom_sheet.xml | 87 --------- core/src/main/res/values/dimens.xml | 12 -- 8 files changed, 238 insertions(+), 195 deletions(-) create mode 100644 core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt delete mode 100644 core/src/main/res/layout/layout_donation_bottom_sheet.xml diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index ce59bda94..60038e324 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -25,6 +25,5 @@ 8dp 8dp 2dp - 48dp diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt index 04c055a2e..64fa0ca93 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt @@ -39,18 +39,13 @@ import android.os.Looper import android.provider.Settings import android.util.AttributeSet import android.view.ActionMode -import android.view.Gravity.BOTTOM -import android.view.Gravity.CENTER_HORIZONTAL import android.view.LayoutInflater import android.view.Menu import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE import android.view.ViewGroup import android.webkit.WebBackForwardList import android.webkit.WebView import android.widget.FrameLayout -import android.widget.TextView import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -106,7 +101,6 @@ import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.consumeObservable import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.hasNotificationPermission -import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isLandScapeMode import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.observeNavigationResult import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.requestNotificationPermission import org.kiwix.kiwixmobile.core.extensions.closeFullScreenMode @@ -155,7 +149,6 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchItemToOpen import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon import org.kiwix.kiwixmobile.core.ui.models.IconItem import org.kiwix.kiwixmobile.core.ui.theme.White -import org.kiwix.kiwixmobile.core.utils.DimenUtils.getWindowWidth import org.kiwix.kiwixmobile.core.utils.DonationDialogHandler import org.kiwix.kiwixmobile.core.utils.DonationDialogHandler.ShowDonationDialogCallback import org.kiwix.kiwixmobile.core.utils.ExternalLinkOpener @@ -277,7 +270,6 @@ abstract class CoreReaderFragment : private var isFirstRun = false private var tableDrawerAdapter: TableDrawerAdapter? = null private var tableDrawerRight: RecyclerView? = null - private var donationLayout: FrameLayout? = null private var bookmarkingJob: Job? = null private var isBookmarked = false private lateinit var serviceConnection: ServiceConnection @@ -339,7 +331,10 @@ abstract class CoreReaderFragment : shouldShowFullScreenMode = false, searchPlaceHolderItemForCustomApps = false to { openSearch(searchString = "", isOpenedFromTabView = false, false) - } + }, + appName = "", + donateButtonClick = {}, + laterButtonClick = {} ) ) private var readerLifeCycleScope: CoroutineScope? = null @@ -453,7 +448,17 @@ abstract class CoreReaderFragment : readerScreenTitle = context.getString(R.string.reader), darkModeViewPainter = darkModeViewPainter, fullScreenItem = fullScreenItem.first to getVideoView(), - tocButtonItem = getTocButtonStateAndAction() + tocButtonItem = getTocButtonStateAndAction(), + appName = (requireActivity() as CoreMainActivity).appName, + donateButtonClick = { + donationDialogHandler?.updateLastDonationPopupShownTime() + openKiwixSupportUrl() + readerScreenState.update { copy(shouldShowDonationPopup = false) } + }, + laterButtonClick = { + donationDialogHandler?.donateLater() + readerScreenState.update { copy(shouldShowDonationPopup = false) } + } ) } } @@ -1176,8 +1181,6 @@ abstract class CoreReaderFragment : compatCallback = null drawerLayout = null tableDrawerRightContainer = null - donationLayout?.removeAllViews() - donationLayout = null } private fun updateTableOfContents() { @@ -1908,88 +1911,14 @@ abstract class CoreReaderFragment : lifecycleScope.launch { donationDialogHandler?.attemptToShowDonationPopup() } } - @Suppress("InflateParams", "MagicNumber") protected open fun showDonationLayout() { - val donationCardView = layoutInflater.inflate(R.layout.layout_donation_bottom_sheet, null) - val layoutParams = FrameLayout.LayoutParams( - getDonationPopupWidth(), - FrameLayout.LayoutParams.WRAP_CONTENT - ).apply { - val rightAndLeftMargin = requireActivity().resources.getDimensionPixelSize( - R.dimen.activity_horizontal_margin - ) - setMargins( - rightAndLeftMargin, - 0, - rightAndLeftMargin, - getBottomMarginForDonationPopup() - ) - gravity = BOTTOM or CENTER_HORIZONTAL - } - - donationCardView.layoutParams = layoutParams - donationLayout?.apply { - removeAllViews() - addView(donationCardView) - setDonationLayoutVisibility(VISIBLE) - } - donationCardView.findViewById(R.id.descriptionText).apply { - text = getString( - R.string.donation_dialog_description, - (requireActivity() as CoreMainActivity).appName - ) - } - val donateButton: TextView = donationCardView.findViewById(R.id.donateButton) - donateButton.setOnClickListener { - donationDialogHandler?.updateLastDonationPopupShownTime() - setDonationLayoutVisibility(GONE) - openKiwixSupportUrl() - } - - val laterButton: TextView = donationCardView.findViewById(R.id.laterButton) - laterButton.setOnClickListener { - donationDialogHandler?.donateLater() - setDonationLayoutVisibility(GONE) - } - } - - private fun getDonationPopupWidth(): Int { - val deviceWidth = requireActivity().getWindowWidth() - val maximumDonationLayoutWidth = - requireActivity().resources.getDimensionPixelSize(R.dimen.maximum_donation_popup_width) - return when { - deviceWidth > maximumDonationLayoutWidth || requireActivity().isLandScapeMode() -> { - maximumDonationLayoutWidth - } - - else -> FrameLayout.LayoutParams.MATCH_PARENT - } - } - - private fun getBottomMarginForDonationPopup(): Int { - var bottomMargin = requireActivity().resources.getDimensionPixelSize( - R.dimen.donation_popup_bottom_margin - ) - if (readerScreenState.value.shouldShowBottomAppBar) { - // if bottomAppBar is visible then add the height of the bottomAppBar. - bottomMargin += - requireActivity().resources.getDimensionPixelSize( - R.dimen.material_minimum_height_and_width - ) - bottomMargin += requireActivity().resources.getDimensionPixelSize(R.dimen.card_margin) - } - - return bottomMargin + readerScreenState.update { copy(shouldShowDonationPopup = true) } } protected open fun openKiwixSupportUrl() { (requireActivity() as CoreMainActivity).openSupportKiwixExternalLink() } - private fun setDonationLayoutVisibility(visibility: Int) { - donationLayout?.visibility = visibility - } - private fun openFullScreenIfEnabled() { if (isInFullScreenMode()) { openFullScreen() diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt new file mode 100644 index 000000000..23f06fb6b --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt @@ -0,0 +1,173 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.main.reader + +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.ui.theme.DenimBlue800 +import org.kiwix.kiwixmobile.core.ui.theme.White +import org.kiwix.kiwixmobile.core.ui.theme.dimHighlightedTextLight +import org.kiwix.kiwixmobile.core.utils.ComposeDimens + +@Composable +fun DonationLayout( + appName: String, + onDonateButtonClick: () -> Unit, + onLaterButtonClick: () -> Unit +) { + Column( + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.CenterHorizontally + ) { + DonationDialogComposable( + appName, + onDonateButtonClick, + onLaterButtonClick + ) + } +} + +@Composable +fun DonationDialogComposable( + appName: String, + onDonateButtonClick: () -> Unit, + onLaterButtonClick: () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(all = ComposeDimens.SIXTEEN_DP), + shape = MaterialTheme.shapes.medium, + elevation = CardDefaults.cardElevation(defaultElevation = ComposeDimens.SIX_DP), + colors = CardDefaults.cardColors( + containerColor = White + ) + ) { + Column( + modifier = Modifier + .padding( + start = ComposeDimens.SIXTEEN_DP, + end = ComposeDimens.SIXTEEN_DP, + top = ComposeDimens.SIXTEEN_DP + ) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { + Image( + painter = painterResource(id = R.drawable.ic_donation_icon), + contentDescription = stringResource(id = R.string.donation_dialog_title), + modifier = Modifier + .size(ComposeDimens.FIFTY_DP) + ) + Spacer(modifier = Modifier.width(ComposeDimens.TWELVE_DP)) + Column { + DonationDialogHeadingText() + DonationDialogSubHeadingText(appName = appName) + } + } + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.weight(1f)) + + DonationDialogButton( + onButtonClick = onLaterButtonClick, + buttonText = R.string.rate_dialog_neutral + ) + DonationDialogButton( + onButtonClick = onDonateButtonClick, + buttonText = R.string.make_donation + ) + } + } + } +} + +@Composable +fun DonationDialogButton( + onButtonClick: () -> Unit, + @StringRes buttonText: Int +) { + TextButton( + onClick = onButtonClick + ) { + Text( + text = stringResource(buttonText), + color = DenimBlue800 + ) + } +} + +@Composable +fun DonationDialogHeadingText() { + Text( + text = stringResource(id = R.string.donation_dialog_title), + style = MaterialTheme.typography.titleMedium, + fontSize = ComposeDimens.SMALL_TITLE_TEXT_SIZE + ) +} + +@Composable +fun DonationDialogSubHeadingText(appName: String) { + Text( + text = stringResource( + R.string.donation_dialog_description, + appName + ), + fontSize = ComposeDimens.FOURTEEN_SP, + color = dimHighlightedTextLight, + modifier = Modifier.padding(top = ComposeDimens.FOUR_DP) + ) +} + +@Preview() +@Composable +fun DonationDialogComposablePreview() { + DonationLayout( + appName = "kiwix", + onDonateButtonClick = {}, + onLaterButtonClick = {} + ) +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt index c8cb7cd7c..9e4036770 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt @@ -18,6 +18,7 @@ package org.kiwix.kiwixmobile.core.main.reader +import android.content.res.Configuration import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -89,6 +90,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalWindowInfo @@ -130,9 +132,11 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.BACK_TO_TOP_BUTTON_BOTTOM_ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_ALL_TAB_BUTTON_BOTTOM_PADDING import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_TAB_ICON_ANIMATION_TIMEOUT import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_TAB_ICON_SIZE +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.DONATION_LAYOUT_MAXIMUM_WIDTH import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.KIWIX_TOOLBAR_HEIGHT import org.kiwix.kiwixmobile.core.utils.ComposeDimens.ONE_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.READER_BOTTOM_APP_BAR_BUTTON_ICON_SIZE import org.kiwix.kiwixmobile.core.utils.ComposeDimens.READER_BOTTOM_APP_BAR_DISABLE_BUTTON_ALPHA @@ -235,6 +239,7 @@ private fun ReaderContentLayout( ShowProgressBarIfZIMFilePageIsLoading(state) Column(Modifier.align(Alignment.BottomCenter)) { TtsControls(state) + ShowDonationLayout(state) BottomAppBarOfReaderScreen( state.bookmarkButtonItem, state.previousPageButtonItem, @@ -251,7 +256,6 @@ private fun ReaderContentLayout( ) } } - ShowDonationLayout(state) } } } @@ -585,18 +589,42 @@ private fun BottomAppBarButtonIcon( } @Composable -private fun BoxScope.ShowDonationLayout(state: ReaderScreenState) { +private fun ShowDonationLayout(state: ReaderScreenState) { if (state.shouldShowDonationPopup) { + val popupWidth = getDonationPopupWidth() Box( modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() + .then( + if (popupWidth != Dp.Unspecified) { + Modifier.width(popupWidth) + } else { + Modifier.fillMaxWidth() + } + ) + .padding(horizontal = SIXTEEN_DP) ) { - // TODO create donation popup layout. + DonationLayout( + state.appName, + state.donateButtonClick, + state.laterButtonClick + ) } } } +@Composable +private fun getDonationPopupWidth(): Dp { + val configuration = LocalWindowInfo.current + val screenWidth = configuration.containerSize.width.dp + val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + + return if (screenWidth > DONATION_LAYOUT_MAXIMUM_WIDTH || isLandscape) { + DONATION_LAYOUT_MAXIMUM_WIDTH + } else { + Dp.Unspecified + } +} + @Composable fun TabSwitcherView( webViews: List, @@ -703,7 +731,6 @@ private fun BoxScope.CloseAllTabButton(onCloseAllTabs: () -> Unit) { } } -@Suppress("MagicNumber") @Composable fun TabItemView( index: Int, @@ -715,7 +742,7 @@ fun TabItemView( ) { val cardElevation = if (isSelected) EIGHT_DP else TWO_DP val borderColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent - val (cardWidth, cardHeight) = getTabCardSize(toolbarHeightDp = 56.dp) + val (cardWidth, cardHeight) = getTabCardSize(toolbarHeightDp = KIWIX_TOOLBAR_HEIGHT) Box(modifier = modifier) { Column( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreenState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreenState.kt index d6a9438da..e6edbfd63 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreenState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreenState.kt @@ -156,5 +156,17 @@ data class ReaderScreenState( /** * Manages the showing/hiding of search placeholder in toolbar for custom apps. */ - val searchPlaceHolderItemForCustomApps: Pair Unit> + val searchPlaceHolderItemForCustomApps: Pair Unit>, + /** + * Manages the showing of application name in donation layout. + */ + val appName: String, + /** + * Handles the click when user clicks on "Make a donation" button. + */ + val donateButtonClick: () -> Unit, + /** + * Handles the click when user clicks on "Later" button in donation layout. + */ + val laterButtonClick: () -> Unit ) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt index ce8365f9c..32cd73ee2 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt @@ -37,6 +37,7 @@ object ComposeDimens { // KiwixAppBar(Toolbar) dimens val ACTION_MENU_TEXTVIEW_BUTTON_PADDING = 13.dp + val KIWIX_TOOLBAR_HEIGHT = 56.dp // Padding & Margins val SIXTY_DP = 60.dp @@ -193,4 +194,5 @@ object ComposeDimens { val BACK_TO_TOP_BUTTON_BOTTOM_MARGIN = 80.dp const val READER_BOTTOM_APP_BAR_DISABLE_BUTTON_ALPHA = 0.38f val SEARCH_PLACEHOLDER_TEXT_SIZE = 12.sp + val DONATION_LAYOUT_MAXIMUM_WIDTH = 400.dp } diff --git a/core/src/main/res/layout/layout_donation_bottom_sheet.xml b/core/src/main/res/layout/layout_donation_bottom_sheet.xml deleted file mode 100644 index c89f59cd3..000000000 --- a/core/src/main/res/layout/layout_donation_bottom_sheet.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml index 19e37eea4..5d0282ae5 100644 --- a/core/src/main/res/values/dimens.xml +++ b/core/src/main/res/values/dimens.xml @@ -8,24 +8,12 @@ 10dp 50dp 10dip - 7dp 1dp - 2dp 16dp 16dp 0dp 56dp - 5dp - 25dp - - 48dp - 36dp 48dp - 24dp - 13dp - 20dp 400dp - 150dp - 10dp From bea28a16a3cc087549879a399a5c1abba469210e Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Sat, 28 Jun 2025 01:08:25 +0530 Subject: [PATCH 2/3] Fixed: The donation layout was not respecting the dark theme. * Fixed: The alignment of donation layout. * Improved the `DonationLayout` code for better readability and maintainability. --- .../core/main/reader/DonationLayout.kt | 135 +++++++++++------- .../core/main/reader/ReaderScreen.kt | 29 +--- 2 files changed, 81 insertions(+), 83 deletions(-) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt index 23f06fb6b..564ceedb6 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt @@ -18,6 +18,7 @@ package org.kiwix.kiwixmobile.core.main.reader +import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement @@ -37,14 +38,17 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.ui.theme.DenimBlue800 -import org.kiwix.kiwixmobile.core.ui.theme.White -import org.kiwix.kiwixmobile.core.ui.theme.dimHighlightedTextLight import org.kiwix.kiwixmobile.core.utils.ComposeDimens +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.DONATION_LAYOUT_MAXIMUM_WIDTH +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP @Composable fun DonationLayout( @@ -52,11 +56,21 @@ fun DonationLayout( onDonateButtonClick: () -> Unit, onLaterButtonClick: () -> Unit ) { + val donationLayoutWidth = getDonationLayoutWidth() Column( verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .then( + if (donationLayoutWidth != Dp.Unspecified) { + Modifier.width(donationLayoutWidth) + } else { + Modifier.fillMaxWidth() + } + ) + .padding(horizontal = SIXTEEN_DP), ) { - DonationDialogComposable( + DonationDialogCard( appName, onDonateButtonClick, onLaterButtonClick @@ -65,7 +79,7 @@ fun DonationLayout( } @Composable -fun DonationDialogComposable( +fun DonationDialogCard( appName: String, onDonateButtonClick: () -> Unit, onLaterButtonClick: () -> Unit @@ -74,57 +88,65 @@ fun DonationDialogComposable( modifier = Modifier .fillMaxWidth() .wrapContentHeight() - .padding(all = ComposeDimens.SIXTEEN_DP), + .padding(ComposeDimens.SIXTEEN_DP), shape = MaterialTheme.shapes.medium, elevation = CardDefaults.cardElevation(defaultElevation = ComposeDimens.SIX_DP), - colors = CardDefaults.cardColors( - containerColor = White - ) + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer) ) { Column( modifier = Modifier - .padding( - start = ComposeDimens.SIXTEEN_DP, - end = ComposeDimens.SIXTEEN_DP, - top = ComposeDimens.SIXTEEN_DP - ) + .padding(horizontal = ComposeDimens.SIXTEEN_DP) + .padding(top = ComposeDimens.SIXTEEN_DP) ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.Top - ) { - Image( - painter = painterResource(id = R.drawable.ic_donation_icon), - contentDescription = stringResource(id = R.string.donation_dialog_title), - modifier = Modifier - .size(ComposeDimens.FIFTY_DP) - ) - Spacer(modifier = Modifier.width(ComposeDimens.TWELVE_DP)) - Column { - DonationDialogHeadingText() - DonationDialogSubHeadingText(appName = appName) - } - } - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(modifier = Modifier.weight(1f)) - - DonationDialogButton( - onButtonClick = onLaterButtonClick, - buttonText = R.string.rate_dialog_neutral - ) - DonationDialogButton( - onButtonClick = onDonateButtonClick, - buttonText = R.string.make_donation - ) - } + DonationDialogContent(appName) + DonationDialogButtons(onDonateButtonClick, onLaterButtonClick) } } } +@Composable +private fun DonationDialogContent(appName: String) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { + Image( + painter = painterResource(id = R.drawable.ic_donation_icon), + contentDescription = stringResource(id = R.string.donation_dialog_title), + modifier = Modifier + .size(ComposeDimens.FIFTY_DP) + ) + Spacer(modifier = Modifier.width(ComposeDimens.TWELVE_DP)) + Column { + DonationDialogHeadingText() + DonationDialogSubHeadingText(appName = appName) + } + } +} + +@Composable +private fun DonationDialogButtons( + onDonateButtonClick: () -> Unit, + onLaterButtonClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.weight(1f)) + + DonationDialogButton( + onButtonClick = onLaterButtonClick, + buttonText = R.string.rate_dialog_neutral + ) + DonationDialogButton( + onButtonClick = onDonateButtonClick, + buttonText = R.string.make_donation + ) + } +} + @Composable fun DonationDialogButton( onButtonClick: () -> Unit, @@ -157,17 +179,20 @@ fun DonationDialogSubHeadingText(appName: String) { appName ), fontSize = ComposeDimens.FOURTEEN_SP, - color = dimHighlightedTextLight, + color = MaterialTheme.colorScheme.onTertiary, modifier = Modifier.padding(top = ComposeDimens.FOUR_DP) ) } -@Preview() @Composable -fun DonationDialogComposablePreview() { - DonationLayout( - appName = "kiwix", - onDonateButtonClick = {}, - onLaterButtonClick = {} - ) +private fun getDonationLayoutWidth(): Dp { + val configuration = LocalWindowInfo.current + val screenWidth = configuration.containerSize.width.dp + val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + + return if (screenWidth > DONATION_LAYOUT_MAXIMUM_WIDTH || isLandscape) { + DONATION_LAYOUT_MAXIMUM_WIDTH + } else { + Dp.Unspecified + } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt index 9e4036770..b9a77c81d 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt @@ -18,7 +18,6 @@ package org.kiwix.kiwixmobile.core.main.reader -import android.content.res.Configuration import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -90,7 +89,6 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalWindowInfo @@ -132,7 +130,6 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.BACK_TO_TOP_BUTTON_BOTTOM_ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_ALL_TAB_BUTTON_BOTTOM_PADDING import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_TAB_ICON_ANIMATION_TIMEOUT import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_TAB_ICON_SIZE -import org.kiwix.kiwixmobile.core.utils.ComposeDimens.DONATION_LAYOUT_MAXIMUM_WIDTH import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP @@ -591,18 +588,7 @@ private fun BottomAppBarButtonIcon( @Composable private fun ShowDonationLayout(state: ReaderScreenState) { if (state.shouldShowDonationPopup) { - val popupWidth = getDonationPopupWidth() - Box( - modifier = Modifier - .then( - if (popupWidth != Dp.Unspecified) { - Modifier.width(popupWidth) - } else { - Modifier.fillMaxWidth() - } - ) - .padding(horizontal = SIXTEEN_DP) - ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) { DonationLayout( state.appName, state.donateButtonClick, @@ -612,19 +598,6 @@ private fun ShowDonationLayout(state: ReaderScreenState) { } } -@Composable -private fun getDonationPopupWidth(): Dp { - val configuration = LocalWindowInfo.current - val screenWidth = configuration.containerSize.width.dp - val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE - - return if (screenWidth > DONATION_LAYOUT_MAXIMUM_WIDTH || isLandscape) { - DONATION_LAYOUT_MAXIMUM_WIDTH - } else { - Dp.Unspecified - } -} - @Composable fun TabSwitcherView( webViews: List, From 1e9e634fca23f4d51c2c4386afe386aca9eee6cd Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Sat, 28 Jun 2025 20:11:13 +0530 Subject: [PATCH 3/3] Added `DonationDialogTest` UI test cases to thoroughly verify the functionality using Compose UI. * Minor improvements to dark mode styling for the donation layout buttons. --- .../kiwixmobile/reader/DonationDialogTest.kt | 229 ++++++++++++++++++ .../kiwix/kiwixmobile/reader/DonationRobot.kt | 45 ++++ .../core/main/reader/DonationLayout.kt | 17 +- .../core/utils/DonationDialogHandlerTest.kt | 2 +- 4 files changed, 284 insertions(+), 9 deletions(-) create mode 100644 app/src/androidTest/java/org/kiwix/kiwixmobile/reader/DonationDialogTest.kt create mode 100644 app/src/androidTest/java/org/kiwix/kiwixmobile/reader/DonationRobot.kt diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/DonationDialogTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/DonationDialogTest.kt new file mode 100644 index 000000000..12f20ef83 --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/DonationDialogTest.kt @@ -0,0 +1,229 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.reader + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.core.content.edit +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.accessibility.AccessibilityChecks +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.internal.runner.junit4.statement.UiThreadStatement +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesCheck +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews +import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck +import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.anyOf +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.kiwix.kiwixmobile.BaseActivityTest +import org.kiwix.kiwixmobile.R +import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.THREE_MONTHS_IN_MILLISECONDS +import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER +import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER +import org.kiwix.kiwixmobile.main.KiwixMainActivity +import org.kiwix.kiwixmobile.nav.destination.library.library +import org.kiwix.kiwixmobile.testutils.RetryRule +import org.kiwix.kiwixmobile.testutils.TestUtils +import org.kiwix.kiwixmobile.testutils.TestUtils.closeSystemDialogs +import org.kiwix.kiwixmobile.testutils.TestUtils.isSystemUINotRespondingDialogVisible +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream + +class DonationDialogTest : BaseActivityTest() { + @Rule(order = RETRY_RULE_ORDER) + @JvmField + val retryRule = RetryRule() + + @get:Rule(order = COMPOSE_TEST_RULE_ORDER) + val composeTestRule = createComposeRule() + + private lateinit var kiwixMainActivity: KiwixMainActivity + private lateinit var sharedPreferenceUtil: SharedPreferenceUtil + + @Before + override fun waitForIdle() { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { + if (isSystemUINotRespondingDialogVisible(this)) { + closeSystemDialogs(context, this) + } + waitForIdle() + } + PreferenceManager.getDefaultSharedPreferences(context).edit { + putBoolean(SharedPreferenceUtil.PREF_SHOW_INTRO, false) + putBoolean(SharedPreferenceUtil.PREF_WIFI_ONLY, false) + putBoolean(SharedPreferenceUtil.PREF_IS_TEST, true) + putString(SharedPreferenceUtil.PREF_LANG, "en") + } + sharedPreferenceUtil = SharedPreferenceUtil(context) + activityScenario = + ActivityScenario.launch(KiwixMainActivity::class.java).apply { + moveToState(Lifecycle.State.RESUMED) + onActivity { + handleLocaleChange( + it, + "en", + sharedPreferenceUtil + ) + } + } + } + + init { + AccessibilityChecks.enable().apply { + setRunChecksFromRootView(true) + setSuppressingResultMatcher( + anyOf( + allOf( + matchesCheck(TouchTargetSizeCheck::class.java), + matchesViews(withContentDescription("More options")) + ), + matchesCheck(SpeakableTextPresentCheck::class.java) + ) + ) + } + } + + @Test + fun showDonationPopupWhenApplicationIsThreeMonthOldAndHaveAtleastOneZIMFile() { + loadZIMFileInApplication() + sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds = 0L + sharedPreferenceUtil.laterClickedMilliSeconds = 0L + openReaderFragment() + donation { assertDonationDialogDisplayed(composeTestRule) } + } + + @Test + fun shouldNotShowDonationPopupWhenApplicationIsThreeMonthOldAndDoNotHaveAnyZIMFile() { + sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds = 0L + activityScenario.onActivity { + kiwixMainActivity = it + kiwixMainActivity.navigate(R.id.libraryFragment) + } + deleteAllZIMFilesFromApplication() + openReaderFragment() + donation { assertDonationDialogIsNotDisplayed(composeTestRule) } + } + + @Test + fun shouldNotShowPopupIfTimeSinceLastPopupIsLessThanThreeMonth() { + sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds = + System.currentTimeMillis() - (THREE_MONTHS_IN_MILLISECONDS / 2) + loadZIMFileInApplication() + openReaderFragment() + donation { assertDonationDialogIsNotDisplayed(composeTestRule) } + } + + @Test + fun shouldShowDonationPopupIfTimeSinceLastPopupExceedsThreeMonths() { + sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds = + System.currentTimeMillis() - (THREE_MONTHS_IN_MILLISECONDS + 1000) + loadZIMFileInApplication() + openReaderFragment() + donation { assertDonationDialogDisplayed(composeTestRule) } + } + + @Test + fun testShouldShowDonationPopupWhenLaterClickedTimeExceedsThreeMonths() { + sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds = 0L + sharedPreferenceUtil.laterClickedMilliSeconds = + System.currentTimeMillis() - (THREE_MONTHS_IN_MILLISECONDS + 1000) + loadZIMFileInApplication() + openReaderFragment() + donation { assertDonationDialogDisplayed(composeTestRule) } + } + + @Test + fun testShouldNotShowPopupIfLaterClickedTimeIsLessThanThreeMonths() { + sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds = 0L + sharedPreferenceUtil.laterClickedMilliSeconds = + System.currentTimeMillis() - 10000L + loadZIMFileInApplication() + openReaderFragment() + donation { assertDonationDialogIsNotDisplayed(composeTestRule) } + } + + private fun openReaderFragment() { + UiThreadStatement.runOnUiThread { + kiwixMainActivity.navigate(kiwixMainActivity.readerFragmentResId) + } + } + + private fun loadZIMFileInApplication() { + openLocalLibraryScreen() + deleteAllZIMFilesFromApplication() + val loadFileStream = + DonationDialogTest::class.java.classLoader.getResourceAsStream("testzim.zim") + val zimFile = + File( + context.getExternalFilesDirs(null)[0], + "testzim.zim" + ) + if (zimFile.exists()) zimFile.delete() + zimFile.createNewFile() + loadFileStream.use { inputStream -> + val outputStream: OutputStream = FileOutputStream(zimFile) + outputStream.use { it -> + val buffer = ByteArray(inputStream.available()) + var length: Int + while (inputStream.read(buffer).also { length = it } > 0) { + it.write(buffer, 0, length) + } + } + } + refreshZIMFilesList() + } + + private fun openLocalLibraryScreen() { + activityScenario.onActivity { + kiwixMainActivity = it + kiwixMainActivity.navigate(R.id.libraryFragment) + } + } + + private fun refreshZIMFilesList() { + library { + refreshList(composeTestRule) + waitUntilZimFilesRefreshing(composeTestRule) + } + } + + private fun deleteAllZIMFilesFromApplication() { + refreshZIMFilesList() + library { + // delete all the ZIM files showing in the LocalLibrary + // screen to properly test the scenario. + deleteZimIfExists(composeTestRule) + } + } + + @After + fun finish() { + TestUtils.deleteTemporaryFilesOfTestCases(context) + } +} diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/DonationRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/DonationRobot.kt new file mode 100644 index 000000000..c8c6eedff --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/DonationRobot.kt @@ -0,0 +1,45 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.reader + +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.isNotDisplayed +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onNodeWithTag +import applyWithViewHierarchyPrinting +import org.kiwix.kiwixmobile.BaseRobot +import org.kiwix.kiwixmobile.core.main.reader.DONATION_LAYOUT_TESTING_TAG +import org.kiwix.kiwixmobile.testutils.TestUtils.waitUntilTimeout + +fun donation(func: DonationRobot.() -> Unit) = DonationRobot().applyWithViewHierarchyPrinting(func) +class DonationRobot : BaseRobot() { + fun assertDonationDialogDisplayed(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntilTimeout() + onNodeWithTag(DONATION_LAYOUT_TESTING_TAG).isDisplayed() + } + } + + fun assertDonationDialogIsNotDisplayed(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntilTimeout() + onNodeWithTag(DONATION_LAYOUT_TESTING_TAG).isNotDisplayed() + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt index 564ceedb6..7c835c0f3 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/DonationLayout.kt @@ -40,16 +40,18 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.kiwix.kiwixmobile.core.R -import org.kiwix.kiwixmobile.core.ui.theme.DenimBlue800 import org.kiwix.kiwixmobile.core.utils.ComposeDimens import org.kiwix.kiwixmobile.core.utils.ComposeDimens.DONATION_LAYOUT_MAXIMUM_WIDTH import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP +const val DONATION_LAYOUT_TESTING_TAG = "donationLayoutTestingTag" + @Composable fun DonationLayout( appName: String, @@ -70,7 +72,7 @@ fun DonationLayout( ) .padding(horizontal = SIXTEEN_DP), ) { - DonationDialogCard( + DonationLayoutCard( appName, onDonateButtonClick, onLaterButtonClick @@ -79,7 +81,7 @@ fun DonationLayout( } @Composable -fun DonationDialogCard( +fun DonationLayoutCard( appName: String, onDonateButtonClick: () -> Unit, onLaterButtonClick: () -> Unit @@ -88,7 +90,8 @@ fun DonationDialogCard( modifier = Modifier .fillMaxWidth() .wrapContentHeight() - .padding(ComposeDimens.SIXTEEN_DP), + .padding(ComposeDimens.SIXTEEN_DP) + .testTag(DONATION_LAYOUT_TESTING_TAG), shape = MaterialTheme.shapes.medium, elevation = CardDefaults.cardElevation(defaultElevation = ComposeDimens.SIX_DP), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer) @@ -152,12 +155,10 @@ fun DonationDialogButton( onButtonClick: () -> Unit, @StringRes buttonText: Int ) { - TextButton( - onClick = onButtonClick - ) { + TextButton(onClick = onButtonClick) { Text( text = stringResource(buttonText), - color = DenimBlue800 + color = MaterialTheme.colorScheme.primary ) } } diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/utils/DonationDialogHandlerTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/utils/DonationDialogHandlerTest.kt index 88556f6bc..735ffba25 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/utils/DonationDialogHandlerTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/utils/DonationDialogHandlerTest.kt @@ -154,7 +154,7 @@ class DonationDialogHandlerTest { } @Test - fun `test should show popup if later clicked time is less than three months`() = + fun `test should not show popup if later clicked time is less than three months`() = runTest { donationDialogHandler = spyk(donationDialogHandler) val currentMilliSeconds = System.currentTimeMillis()