From a9becb38038c1944dc9ee39f7bcea85a03619ac6 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Fri, 8 Aug 2025 00:26:55 +0530 Subject: [PATCH] Migrated the `CustomDownloadFragment` to jetpack compose. * Refactored the code to use Jetpack Compose UI. * Removed unused code from the project. * Fixed: When there is no ZIM file available and navigating to `CustomDownloadFragment` application sometimes crashes. * Fixed: "Open Library" button was shown even when no readable ZIM file was available. --- .../core/main/reader/CoreReaderFragment.kt | 2 +- .../kiwixmobile/core/utils/ComposeDimens.kt | 4 + core/src/main/res/layout/drawer_right.xml | 8 - core/src/main/res/layout/nav_main.xml | 29 --- core/src/main/res/layout/section_list.xml | 30 --- .../custom/download/CustomDownloadFragment.kt | 124 +++-------- .../custom/download/CustomDownloadScreen.kt | 203 ++++++++++++++++++ .../custom/main/CustomReaderFragment.kt | 24 ++- .../res/layout/fragment_custom_download.xml | 47 ---- .../layout_custom_download_complete.xml | 20 -- .../layout/layout_custom_download_error.xml | 30 --- .../layout_custom_download_in_progress.xml | 36 ---- .../layout_custom_download_required.xml | 31 --- 13 files changed, 259 insertions(+), 329 deletions(-) delete mode 100644 core/src/main/res/layout/drawer_right.xml delete mode 100644 core/src/main/res/layout/nav_main.xml delete mode 100644 core/src/main/res/layout/section_list.xml create mode 100644 custom/src/main/java/org/kiwix/kiwixmobile/custom/download/CustomDownloadScreen.kt delete mode 100644 custom/src/main/res/layout/fragment_custom_download.xml delete mode 100644 custom/src/main/res/layout/layout_custom_download_complete.xml delete mode 100644 custom/src/main/res/layout/layout_custom_download_error.xml delete mode 100644 custom/src/main/res/layout/layout_custom_download_in_progress.xml delete mode 100644 custom/src/main/res/layout/layout_custom_download_required.xml 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 5013a09cc..85ee32973 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 @@ -1769,7 +1769,7 @@ abstract class CoreReaderFragment : } // opens home screen when user closes all tabs - protected fun showNoBookOpenViews() { + open fun showNoBookOpenViews() { readerScreenState.update { copy(isNoBookOpenInReader = true) } } 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 cc5f66f56..85ed18ad7 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 @@ -195,4 +195,8 @@ object ComposeDimens { // MainActivity dimens val NAVIGATION_DRAWER_WIDTH = 280.dp + + // Custom download screen dimens + val CUSTOM_DOWNLOAD_LAYOUT_TOP_MARGIN = 100.dp + val CUSTOM_DOWNLOAD_PROGRESS_BAR_WIDTH = 200.dp } diff --git a/core/src/main/res/layout/drawer_right.xml b/core/src/main/res/layout/drawer_right.xml deleted file mode 100644 index fc5003d0c..000000000 --- a/core/src/main/res/layout/drawer_right.xml +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/core/src/main/res/layout/nav_main.xml b/core/src/main/res/layout/nav_main.xml deleted file mode 100644 index 0f3fd2a6b..000000000 --- a/core/src/main/res/layout/nav_main.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - diff --git a/core/src/main/res/layout/section_list.xml b/core/src/main/res/layout/section_list.xml deleted file mode 100644 index b5f7dd9ab..000000000 --- a/core/src/main/res/layout/section_list.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/CustomDownloadFragment.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/CustomDownloadFragment.kt index 908e36783..eaa2f237d 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/CustomDownloadFragment.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/CustomDownloadFragment.kt @@ -19,39 +19,33 @@ package org.kiwix.kiwixmobile.custom.download import android.Manifest.permission.POST_NOTIFICATIONS -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView import androidx.core.app.ActivityCompat import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch -import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseFragment import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions -import org.kiwix.kiwixmobile.core.data.remote.isAuthenticationUrl -import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem -import org.kiwix.kiwixmobile.core.downloader.model.DownloadState import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.hasNotificationPermission import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.requestNotificationPermission -import org.kiwix.kiwixmobile.core.extensions.setDistinctDisplayedChild +import org.kiwix.kiwixmobile.core.extensions.update import org.kiwix.kiwixmobile.core.extensions.viewModel import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.navigateToAppSettings import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower +import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog import org.kiwix.kiwixmobile.custom.customActivityComponent -import org.kiwix.kiwixmobile.custom.databinding.FragmentCustomDownloadBinding import org.kiwix.kiwixmobile.custom.download.Action.ClickedDownload import org.kiwix.kiwixmobile.custom.download.Action.ClickedRetry -import org.kiwix.kiwixmobile.custom.download.State.DownloadComplete -import org.kiwix.kiwixmobile.custom.download.State.DownloadFailed -import org.kiwix.kiwixmobile.custom.download.State.DownloadInProgress -import org.kiwix.kiwixmobile.custom.download.State.DownloadRequired import javax.inject.Inject class CustomDownloadFragment : BaseFragment(), FragmentActivityExtensions { @@ -68,8 +62,8 @@ class CustomDownloadFragment : BaseFragment(), FragmentActivityExtensions { var sharedPreferenceUtil: SharedPreferenceUtil? = null @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - - private var fragmentCustomDownloadBinding: FragmentCustomDownloadBinding? = null + private var composeView: ComposeView? = null + private var downloadState = mutableStateOf(State.DownloadRequired) override fun inject(baseActivity: BaseActivity) { baseActivity.customActivityComponent.inject(this) } @@ -80,12 +74,20 @@ class CustomDownloadFragment : BaseFragment(), FragmentActivityExtensions { savedInstanceState: Bundle? ): View? { super.onCreate(savedInstanceState) - fragmentCustomDownloadBinding = - FragmentCustomDownloadBinding.inflate(inflater, container, false) + composeView = ComposeView(requireContext()).apply { + setContent { + CustomDownloadScreen( + state = downloadState.value, + onDownloadClick = { downloadButtonClick() }, + onRetryClick = { retryButtonClick() } + ) + DialogHost(alertDialogShower as AlertDialogShower) + } + } val activity = requireActivity() as CoreMainActivity viewLifecycleOwner.lifecycleScope.launch { downloadViewModel.state.collect { state -> - render(state) + downloadState.update { state } } } @@ -95,26 +97,22 @@ class CustomDownloadFragment : BaseFragment(), FragmentActivityExtensions { effect.invokeWith(activity) } } - return fragmentCustomDownloadBinding?.root + return composeView } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - fragmentCustomDownloadBinding?.apply { - customDownloadRequired.cdDownloadButton.setOnClickListener { - if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) { - performAction(ClickedDownload) - } else { - requestNotificationPermission() - } - } - customDownloadError.cdRetryButton.setOnClickListener { - if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) { - performAction(ClickedRetry) - } else { - requestNotificationPermission() - } - } + private fun downloadButtonClick() { + if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) { + performAction(ClickedDownload) + } else { + requestNotificationPermission() + } + } + + private fun retryButtonClick() { + if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) { + performAction(ClickedRetry) + } else { + requestNotificationPermission() } } @@ -144,63 +142,9 @@ class CustomDownloadFragment : BaseFragment(), FragmentActivityExtensions { activity?.finish() } - private fun render(state: State): Unit? { - return when (state) { - DownloadRequired -> - fragmentCustomDownloadBinding?.cdViewAnimator?.setDistinctDisplayedChild(0) - - is DownloadInProgress -> { - fragmentCustomDownloadBinding?.cdViewAnimator?.setDistinctDisplayedChild(1) - showDownloadingProgress(state.downloads[0]) - } - - is DownloadFailed -> { - fragmentCustomDownloadBinding?.cdViewAnimator?.setDistinctDisplayedChild(2) - val errorMessage = context?.let { context -> - if (state.downloadState.zimUrl?.isAuthenticationUrl == false) { - return@let getErrorMessageFromDownloadState(state.downloadState, context) - } - - val defaultErrorMessage = getErrorMessageFromDownloadState(state.downloadState, context) - // Check if `REQUEST_NOT_SUCCESSFUL` indicates an unsuccessful response from the server. - // If the server does not respond to the URL, we will display a custom message to the user. - if (defaultErrorMessage == context.getString( - R.string.failed_state, - "REQUEST_NOT_SUCCESSFUL" - ) - ) { - context.getString( - R.string.failed_state, - context.getString(R.string.custom_download_error_message_for_authentication_failed) - ) - } else { - defaultErrorMessage - } - } - fragmentCustomDownloadBinding?.customDownloadError?.cdErrorText?.text = errorMessage - } - - DownloadComplete -> - fragmentCustomDownloadBinding?.cdViewAnimator?.setDistinctDisplayedChild(3) - } - } - - private fun getErrorMessageFromDownloadState( - downloadState: DownloadState, - context: Context - ): String = "${downloadState.toReadableState(context)}" - - private fun showDownloadingProgress(downloadItem: DownloadItem) { - fragmentCustomDownloadBinding?.customDownloadInProgress?.apply { - cdDownloadState.text = downloadItem.readableEta - cdEta.text = context?.let(downloadItem.downloadState::toReadableState) - cdProgress.progress = downloadItem.progress - } - } - override fun onDestroyView() { super.onDestroyView() - fragmentCustomDownloadBinding?.root?.removeAllViews() - fragmentCustomDownloadBinding = null + composeView?.disposeComposition() + composeView = null } } diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/CustomDownloadScreen.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/CustomDownloadScreen.kt new file mode 100644 index 000000000..4fc71f733 --- /dev/null +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/CustomDownloadScreen.kt @@ -0,0 +1,203 @@ +/* + * 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.custom.download + +import android.content.Context +import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.wrapContentSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import org.kiwix.kiwixmobile.core.R.string +import org.kiwix.kiwixmobile.core.data.remote.isAuthenticationUrl +import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem +import org.kiwix.kiwixmobile.core.downloader.model.DownloadState +import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar +import org.kiwix.kiwixmobile.core.ui.components.KiwixButton +import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CUSTOM_DOWNLOAD_LAYOUT_TOP_MARGIN +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CUSTOM_DOWNLOAD_PROGRESS_BAR_WIDTH +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIX_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWENTY_DP +import org.kiwix.kiwixmobile.custom.R +import org.kiwix.kiwixmobile.custom.download.State.DownloadComplete +import org.kiwix.kiwixmobile.custom.download.State.DownloadFailed +import org.kiwix.kiwixmobile.custom.download.State.DownloadInProgress +import org.kiwix.kiwixmobile.custom.download.State.DownloadRequired + +@Composable +fun CustomDownloadScreen( + state: State, + onDownloadClick: () -> Unit, + onRetryClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .animateContentSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = CUSTOM_DOWNLOAD_LAYOUT_TOP_MARGIN), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.mipmap.ic_launcher_foreground), + contentDescription = stringResource(id = R.string.app_name), + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterHorizontally) + ) + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Crossfade(targetState = state, label = "download-state") { currentState -> + when (currentState) { + is DownloadRequired -> DownloadRequiredView(onDownloadClick) + is DownloadInProgress -> DownloadInProgressView(currentState.downloads[0]) + is DownloadFailed -> DownloadErrorView(onRetryClick, currentState) + is DownloadComplete -> DownloadCompleteView() + } + } + } + } + } +} + +@Composable +private fun DownloadRequiredView(onDownloadClick: () -> Unit) { + Column( + modifier = Modifier.wrapContentSize(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.invalid_installation), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(TWENTY_DP)) + KiwixButton( + clickListener = onDownloadClick, + buttonText = stringResource(id = string.download) + ) + } +} + +@Composable +private fun DownloadInProgressView(downloadItem: DownloadItem) { + Column( + modifier = Modifier.wrapContentSize(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = downloadItem.readableEta.toString()) + + Spacer(modifier = Modifier.height(TWENTY_DP)) + + Row(verticalAlignment = Alignment.CenterVertically) { + ContentLoadingProgressBar( + progress = downloadItem.progress, + modifier = Modifier + .width(CUSTOM_DOWNLOAD_PROGRESS_BAR_WIDTH) + .height(SIX_DP), + progressBarStyle = ProgressBarStyle.HORIZONTAL + ) + Spacer(modifier = Modifier.width(FIVE_DP)) + Text(text = downloadItem.downloadState.toReadableState(LocalContext.current).toString()) + } + } +} + +@Composable +private fun DownloadErrorView(onRetryClick: () -> Unit, downloadFailed: DownloadFailed) { + val context = LocalContext.current + Column( + modifier = Modifier.wrapContentSize(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = getActualErrorMessage(downloadFailed, context), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(TWENTY_DP)) + KiwixButton( + clickListener = onRetryClick, + buttonText = stringResource(id = R.string.retry) + ) + } +} + +private fun getActualErrorMessage(downloadFailed: DownloadFailed, context: Context): String { + if (downloadFailed.downloadState.zimUrl?.isAuthenticationUrl == false) { + return getErrorMessageFromDownloadState(downloadFailed.downloadState, context) + } + + val defaultErrorMessage = getErrorMessageFromDownloadState(downloadFailed.downloadState, context) + // Check if `REQUEST_NOT_SUCCESSFUL` indicates an unsuccessful response from the server. + // If the server does not respond to the URL, we will display a custom message to the user. + return if (defaultErrorMessage == context.getString( + string.failed_state, + "REQUEST_NOT_SUCCESSFUL" + ) + ) { + context.getString( + string.failed_state, + context.getString(string.custom_download_error_message_for_authentication_failed) + ) + } else { + defaultErrorMessage + } +} + +private fun getErrorMessageFromDownloadState( + downloadState: DownloadState, + context: Context +): String = "${downloadState.toReadableState(context)}" + +@Composable +private fun DownloadCompleteView() { + Box( + modifier = Modifier.wrapContentSize(Alignment.Center), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = stringResource(id = string.complete), + textAlign = TextAlign.Center + ) + } +} diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt index d00b965fc..5e3c83691 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt @@ -20,6 +20,8 @@ package org.kiwix.kiwixmobile.custom.main import android.app.Dialog import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.Menu import android.view.View import androidx.appcompat.app.AppCompatActivity @@ -53,6 +55,8 @@ import java.io.File import java.util.Locale import javax.inject.Inject +const val OPENING_DOWNLOAD_SCREEN_DELAY = 300L + class CustomReaderFragment : CoreReaderFragment() { override fun inject(baseActivity: BaseActivity) { baseActivity.customActivityComponent.inject(this) @@ -288,13 +292,15 @@ class CustomReaderFragment : CoreReaderFragment() { }, onNoFilesFound = { if (sharedPreferenceUtil?.prefIsTest == false) { - val navOptions = NavOptions.Builder() - .setPopUpTo(CustomDestination.Reader.route, true) - .build() - (requireActivity() as CoreMainActivity).navigate( - CustomDestination.Downloads.route, - navOptions - ) + Handler(Looper.getMainLooper()).postDelayed({ + val navOptions = NavOptions.Builder() + .setPopUpTo(CustomDestination.Reader.route, true) + .build() + (requireActivity() as CoreMainActivity).navigate( + CustomDestination.Downloads.route, + navOptions + ) + }, OPENING_DOWNLOAD_SCREEN_DELAY) } } ) @@ -389,6 +395,10 @@ class CustomReaderFragment : CoreReaderFragment() { newMainPageTab() } + override fun showNoBookOpenViews() { + readerScreenState.update { copy(isNoBookOpenInReader = false) } + } + /** * Overrides the method to show the donation popup. When the "Support url" is disabled * in a custom app, this function stop to show the donationPopup. diff --git a/custom/src/main/res/layout/fragment_custom_download.xml b/custom/src/main/res/layout/fragment_custom_download.xml deleted file mode 100644 index 0ba9f2b85..000000000 --- a/custom/src/main/res/layout/fragment_custom_download.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/custom/src/main/res/layout/layout_custom_download_complete.xml b/custom/src/main/res/layout/layout_custom_download_complete.xml deleted file mode 100644 index 649ec1c97..000000000 --- a/custom/src/main/res/layout/layout_custom_download_complete.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/custom/src/main/res/layout/layout_custom_download_error.xml b/custom/src/main/res/layout/layout_custom_download_error.xml deleted file mode 100644 index 406562ae7..000000000 --- a/custom/src/main/res/layout/layout_custom_download_error.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - -