diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityRobot.kt index 66a1b2b32..0bb489cda 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityRobot.kt @@ -20,25 +20,32 @@ package org.kiwix.kiwixmobile.error import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import com.adevinta.android.barista.interaction.BaristaSleepInteractions import org.kiwix.kiwixmobile.BaseRobot -import org.kiwix.kiwixmobile.Findable.StringId.TextId import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.help.SEND_DIAGNOSTIC_REPORT_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils fun errorActivity(func: ErrorActivityRobot.() -> Unit) = ErrorActivityRobot().apply(func) class ErrorActivityRobot : BaseRobot() { - fun assertSendDiagnosticReportDisplayed() { + fun assertSendDiagnosticReportDisplayed(composeTestRule: ComposeContentTestRule) { // Wait a bit for properly visible the HelpFragment. BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong()) - isVisible(TextId(R.string.send_report)) + composeTestRule.apply { + waitForIdle() + onNodeWithTag(SEND_DIAGNOSTIC_REPORT_TESTING_TAG).assertIsDisplayed() + } } - fun clickOnSendDiagnosticReport() { - clickOn(TextId(R.string.send_report)) + fun clickOnSendDiagnosticReport(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(SEND_DIAGNOSTIC_REPORT_TESTING_TAG).performClick() + } } fun assertErrorActivityDisplayed(composeTestRule: ComposeContentTestRule) { diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityTest.kt index 13b683765..d4ae7474a 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityTest.kt @@ -91,8 +91,8 @@ class ErrorActivityTest : BaseActivityTest() { it.navigate(R.id.helpFragment) } errorActivity { - assertSendDiagnosticReportDisplayed() - clickOnSendDiagnosticReport() + assertSendDiagnosticReportDisplayed(composeTestRule) + clickOnSendDiagnosticReport(composeTestRule) assertErrorActivityDisplayed(composeTestRule) // Click on "No, Thanks" button to see it's functionality working or not. clickOnNoThanksButton(composeTestRule) @@ -101,9 +101,9 @@ class ErrorActivityTest : BaseActivityTest() { it.navigate(R.id.helpFragment) } // Assert HelpFragment is visible or not after clicking on the "No, Thanks" button. - assertSendDiagnosticReportDisplayed() + assertSendDiagnosticReportDisplayed(composeTestRule) // Again click on "Send diagnostic report" button to open the ErrorActivity. - clickOnSendDiagnosticReport() + clickOnSendDiagnosticReport(composeTestRule) assertErrorActivityDisplayed(composeTestRule) // Check check boxes are displayed or not. assertCheckBoxesDisplayed(composeTestRule) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/help/HelpFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/help/HelpFragmentTest.kt index ad09c89ea..f3b3b2385 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/help/HelpFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/help/HelpFragmentTest.kt @@ -18,6 +18,7 @@ package org.kiwix.kiwixmobile.help import android.os.Build +import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.espresso.IdlingRegistry @@ -33,6 +34,7 @@ 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.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.testutils.RetryRule @@ -43,6 +45,13 @@ import org.kiwix.kiwixmobile.utils.KiwixIdlingResource class HelpFragmentTest : BaseActivityTest() { private lateinit var sharedPreferenceUtil: SharedPreferenceUtil + @Rule(order = RETRY_RULE_ORDER) + @JvmField + val retryRule = RetryRule() + + @get:Rule(order = COMPOSE_TEST_RULE_ORDER) + val composeTestRule = createComposeRule() + @Before override fun waitForIdle() { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { @@ -66,10 +75,6 @@ class HelpFragmentTest : BaseActivityTest() { } } - @Rule(order = RETRY_RULE_ORDER) - @JvmField - val retryRule = RetryRule() - init { AccessibilityChecks.enable().setRunChecksFromRootView(true) } @@ -81,16 +86,16 @@ class HelpFragmentTest : BaseActivityTest() { it.navigate(R.id.helpFragment) } help { - clickOnWhatDoesKiwixDo() - assertWhatDoesKiwixDoIsExpanded() - clickOnWhatDoesKiwixDo() - clickOnWhereIsContent() - assertWhereIsContentIsExpanded() - clickOnWhereIsContent() - clickOnHowToUpdateContent() - assertHowToUpdateContentIsExpanded() - clickOnHowToUpdateContent() - assertWhyCopyMoveFilesToAppPublicDirectoryIsNotVisible() + clickOnWhatDoesKiwixDo(composeTestRule) + assertWhatDoesKiwixDoIsExpanded(composeTestRule) + clickOnWhatDoesKiwixDo(composeTestRule) + clickOnWhereIsContent(composeTestRule) + assertWhereIsContentIsExpanded(composeTestRule) + clickOnWhereIsContent(composeTestRule) + clickOnHowToUpdateContent(composeTestRule) + assertHowToUpdateContentIsExpanded(composeTestRule) + clickOnHowToUpdateContent(composeTestRule) + assertWhyCopyMoveFilesToAppPublicDirectoryIsNotVisible(composeTestRule) } LeakAssertions.assertNoLeaks() } @@ -103,18 +108,18 @@ class HelpFragmentTest : BaseActivityTest() { it.navigate(R.id.helpFragment) } help { - clickOnWhatDoesKiwixDo() - assertWhatDoesKiwixDoIsExpanded() - clickOnWhatDoesKiwixDo() - clickOnWhereIsContent() - assertWhereIsContentIsExpanded() - clickOnWhereIsContent() - clickOnHowToUpdateContent() - assertHowToUpdateContentIsExpanded() - clickOnHowToUpdateContent() - clickWhyCopyMoveFilesToAppPublicDirectory() - assertWhyCopyMoveFilesToAppPublicDirectoryIsExpanded() - clickWhyCopyMoveFilesToAppPublicDirectory() + clickOnWhatDoesKiwixDo(composeTestRule) + assertWhatDoesKiwixDoIsExpanded(composeTestRule) + clickOnWhatDoesKiwixDo(composeTestRule) + clickOnWhereIsContent(composeTestRule) + assertWhereIsContentIsExpanded(composeTestRule) + clickOnWhereIsContent(composeTestRule) + clickOnHowToUpdateContent(composeTestRule) + assertHowToUpdateContentIsExpanded(composeTestRule) + clickOnHowToUpdateContent(composeTestRule) + clickWhyCopyMoveFilesToAppPublicDirectory(composeTestRule) + assertWhyCopyMoveFilesToAppPublicDirectoryIsExpanded(composeTestRule) + clickWhyCopyMoveFilesToAppPublicDirectory(composeTestRule) } LeakAssertions.assertNoLeaks() } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/help/HelpRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/help/HelpRobot.kt index 63a96e654..f1991e2c5 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/help/HelpRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/help/HelpRobot.kt @@ -17,80 +17,128 @@ */ package org.kiwix.kiwixmobile.help +import androidx.compose.ui.test.assertContentDescriptionEquals +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.withText import org.kiwix.kiwixmobile.BaseRobot -import org.kiwix.kiwixmobile.Findable.StringId.TextId -import org.kiwix.kiwixmobile.Findable.Text -import org.kiwix.kiwixmobile.Findable.ViewId -import org.kiwix.kiwixmobile.core.R.id +import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R.string +import org.kiwix.kiwixmobile.core.help.HELP_SCREEN_ITEM_DESCRIPTION_TESTING_TAG +import org.kiwix.kiwixmobile.core.help.HELP_SCREEN_ITEM_TITLE_TESTING_TAG +import org.kiwix.kiwixmobile.core.ui.components.TOOLBAR_TITLE_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView fun help(func: HelpRobot.() -> Unit) = HelpRobot().apply(func) class HelpRobot : BaseRobot() { - fun assertToolbarDisplayed() { - isVisible(ViewId(id.toolbar)) + fun assertToolbarDisplayed(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(TOOLBAR_TITLE_TESTING_TAG) + .assertTextEquals(context.getString(R.string.menu_help)) + } } - fun clickOnWhatDoesKiwixDo() { - testFlakyView({ onView(withText(string.help_2)).perform(click()) }) + fun clickOnWhatDoesKiwixDo(composeTestRule: ComposeContentTestRule) { + clickOnHelpScreenItemTitle(0, composeTestRule) } - fun assertWhatDoesKiwixDoIsExpanded() { - isVisible( - Text( - helpTextFormat( - string.help_3, - string.help_4 - ) - ) + fun assertWhatDoesKiwixDoIsExpanded(composeTestRule: ComposeContentTestRule) { + assertHelpScreenDescriptionDisplayed( + helpTextFormat(string.help_3, string.help_4), + composeTestRule ) } - fun clickOnWhereIsContent() { - clickOn(TextId(string.help_5)) + fun clickOnWhereIsContent(composeTestRule: ComposeContentTestRule) { + clickOnHelpScreenItemTitle(1, composeTestRule) } - fun assertWhereIsContentIsExpanded() { - isVisible( - Text( - helpTextFormat( - string.help_6, - string.help_7, - string.help_8, - string.help_9, - string.help_10, - string.help_11 - ) - ) + fun assertWhereIsContentIsExpanded(composeTestRule: ComposeContentTestRule) { + assertHelpScreenDescriptionDisplayed( + helpTextFormat( + string.help_6, + string.help_7, + string.help_8, + string.help_9, + string.help_10, + string.help_11 + ), + composeTestRule ) } - fun clickOnHowToUpdateContent() { - clickOn(TextId(string.how_to_update_content)) + fun clickOnHowToUpdateContent(composeTestRule: ComposeContentTestRule) { + clickOnHelpScreenItemTitle(2, composeTestRule) } - fun assertHowToUpdateContentIsExpanded() { - isVisible(TextId(string.update_content_description)) + fun assertHowToUpdateContentIsExpanded(composeTestRule: ComposeContentTestRule) { + assertHelpScreenDescriptionDisplayed( + context.getString(string.update_content_description), + composeTestRule + ) } - fun clickWhyCopyMoveFilesToAppPublicDirectory() { - clickOn(TextId(string.why_copy_move_files_to_app_directory)) + fun clickWhyCopyMoveFilesToAppPublicDirectory(composeTestRule: ComposeContentTestRule) { + clickOnHelpScreenItemTitle(3, composeTestRule) } - fun assertWhyCopyMoveFilesToAppPublicDirectoryIsExpanded() { - isVisible(Text(context.getString(string.copy_move_files_to_app_directory_description))) + fun assertWhyCopyMoveFilesToAppPublicDirectoryIsExpanded(composeTestRule: ComposeContentTestRule) { + assertHelpScreenDescriptionDisplayed( + context.getString(string.copy_move_files_to_app_directory_description), + composeTestRule + ) } - fun assertWhyCopyMoveFilesToAppPublicDirectoryIsNotVisible() { + fun assertWhyCopyMoveFilesToAppPublicDirectoryIsNotVisible(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitForIdle() + val itemTitleList = onAllNodesWithTag(HELP_SCREEN_ITEM_TITLE_TESTING_TAG) + val itemCount = itemTitleList.fetchSemanticsNodes().size + repeat(itemCount) { index -> + try { + itemTitleList[index] + .assertTextEquals(context.getString(string.why_copy_move_files_to_app_directory)) + // If "Why copy/move files to app public directory?" item is visible throw the error. + throw RuntimeException("\"Why copy/move files to app public directory?\" help item is visible in non-playStore variant") + } catch (_: AssertionError) { + // If not found then nothing will do. + } + } + } onView(withText(string.why_copy_move_files_to_app_directory)) .check(doesNotExist()) } + private fun clickOnHelpScreenItemTitle(index: Int, composeTestRule: ComposeContentTestRule) { + testFlakyView({ + composeTestRule.apply { + waitForIdle() + val itemTitleList = onAllNodesWithTag(HELP_SCREEN_ITEM_TITLE_TESTING_TAG) + itemTitleList[index].performClick() + } + }) + } + + private fun assertHelpScreenDescriptionDisplayed( + description: String, + composeTestRule: ComposeContentTestRule + ) { + testFlakyView({ + composeTestRule.apply { + waitForIdle() + onNodeWithTag(HELP_SCREEN_ITEM_DESCRIPTION_TESTING_TAG) + .assertContentDescriptionEquals(description) + } + }) + } + private fun helpTextFormat(vararg stringIds: Int) = stringIds.joinToString(separator = "\n", transform = context::getString) } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/main/TopLevelDestinationTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/main/TopLevelDestinationTest.kt index 07f24ba55..9b60a862c 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/main/TopLevelDestinationTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/main/TopLevelDestinationTest.kt @@ -130,7 +130,7 @@ class TopLevelDestinationTest : BaseActivityTest() { } clickHostBooksOnSideNav(ZimHostRobot::assertMenuWifiHotspotDiplayed) clickSettingsOnSideNav(SettingsRobot::assertMenuSettingsDisplayed) - clickHelpOnSideNav(HelpRobot::assertToolbarDisplayed) + clickHelpOnSideNav { HelpRobot().assertToolbarDisplayed(composeTestRule) } clickSupportKiwixOnSideNav() pressBack() } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/shortcuts/GetContentShortcutTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/shortcuts/GetContentShortcutTest.kt index 800b8ef16..e50f27be0 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/shortcuts/GetContentShortcutTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/shortcuts/GetContentShortcutTest.kt @@ -134,7 +134,7 @@ class GetContentShortcutTest { } clickHostBooksOnSideNav(ZimHostRobot::assertMenuWifiHotspotDiplayed) clickSettingsOnSideNav(SettingsRobot::assertMenuSettingsDisplayed) - clickHelpOnSideNav(HelpRobot::assertToolbarDisplayed) + clickHelpOnSideNav { HelpRobot().assertToolbarDisplayed(composeTestRule) } clickSupportKiwixOnSideNav() pressBack() } diff --git a/app/src/main/res/navigation/kiwix_nav_graph.xml b/app/src/main/res/navigation/kiwix_nav_graph.xml index 60f5534e0..13be224cf 100644 --- a/app/src/main/res/navigation/kiwix_nav_graph.xml +++ b/app/src/main/res/navigation/kiwix_nav_graph.xml @@ -129,8 +129,7 @@ + android:label="HelpFragment" /> - * 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.help - -import android.animation.ObjectAnimator -import android.text.method.LinkMovementMethod -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.core.view.isGone -import androidx.recyclerview.widget.RecyclerView -import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder -import org.kiwix.kiwixmobile.core.databinding.ItemHelpBinding -import org.kiwix.kiwixmobile.core.utils.AnimationUtils.collapse -import org.kiwix.kiwixmobile.core.utils.AnimationUtils.expand - -internal class HelpAdapter(titleDescriptionMap: Map) : - RecyclerView.Adapter() { - private var helpItems = titleDescriptionMap.map { (key, value) -> HelpItem(key, value) } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): Item = Item(ItemHelpBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - - override fun onBindViewHolder( - holder: Item, - position: Int - ) { - holder.bind(helpItems[position]) - } - - override fun getItemCount(): Int = helpItems.size - - internal inner class Item(private val itemHelpBinding: ItemHelpBinding) : - BaseViewHolder(itemHelpBinding.root) { - @SuppressWarnings("MagicNumber") - fun toggleDescriptionVisibility() { - if (itemHelpBinding.itemHelpDescription.isGone) { - ObjectAnimator.ofFloat(itemHelpBinding.itemHelpToggleExpand, "rotation", 0f, 180f).start() - itemHelpBinding.itemHelpDescription.expand() - } else { - ObjectAnimator.ofFloat(itemHelpBinding.itemHelpToggleExpand, "rotation", 180f, 360f).start() - itemHelpBinding.itemHelpDescription.collapse() - } - } - - override fun bind(item: HelpItem) { - itemHelpBinding.itemHelpTitle.setOnClickListener { toggleDescriptionVisibility() } - itemHelpBinding.itemHelpToggleExpand.setOnClickListener { toggleDescriptionVisibility() } - itemHelpBinding.itemHelpDescription.apply { - text = item.description - movementMethod = LinkMovementMethod.getInstance() - } - itemHelpBinding.itemHelpTitle.text = item.title - } - } -} - -class HelpItem(val title: String, val description: String) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpFragment.kt index 8a059b9dd..8fdca13f8 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpFragment.kt @@ -17,20 +17,16 @@ */ package org.kiwix.kiwixmobile.core.help +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar -import androidx.recyclerview.widget.DividerItemDecoration -import org.kiwix.kiwixmobile.core.R +import androidx.compose.ui.platform.ComposeView import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseFragment -import org.kiwix.kiwixmobile.core.databinding.FragmentHelpBinding -import org.kiwix.kiwixmobile.core.error.DiagnosticReportActivity -import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.start import org.kiwix.kiwixmobile.core.main.CoreMainActivity +import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import javax.inject.Inject @@ -38,64 +34,47 @@ import javax.inject.Inject abstract class HelpFragment : BaseFragment() { @Inject lateinit var sharedPreferenceUtil: SharedPreferenceUtil - private var fragmentHelpBinding: FragmentHelpBinding? = null + + // Each subclass is responsible for providing its own raw data. protected open fun rawTitleDescriptionMap(): List> = emptyList() - override val fragmentToolbar: Toolbar? by lazy { - fragmentHelpBinding?.root?.findViewById(R.id.toolbar) - } - override val fragmentTitle: String? by lazy { getString(R.string.menu_help) } - - private val titleDescriptionMap by lazy { - rawTitleDescriptionMap().associate { (title, description) -> - val descriptionValue = - when (description) { - is String -> description - is Int -> resources.getStringArray(description).joinToString(separator = "\n") - else -> { - throw IllegalArgumentException("Invalid description resource type for title: $title") - } - } - - getString(title) to descriptionValue - } - } override fun inject(baseActivity: BaseActivity) { (baseActivity as CoreMainActivity).cachedComponent.inject(this) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val activity = requireActivity() as AppCompatActivity - fragmentHelpBinding?.activityHelpDiagnosticImageView?.setOnClickListener { - sendDiagnosticReport() - } - fragmentHelpBinding?.activityHelpDiagnosticTextView?.setOnClickListener { - sendDiagnosticReport() - } - fragmentHelpBinding?.activityHelpRecyclerView?.addItemDecoration( - DividerItemDecoration(activity, DividerItemDecoration.VERTICAL) - ) - fragmentHelpBinding?.activityHelpRecyclerView?.adapter = HelpAdapter(titleDescriptionMap) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - fragmentHelpBinding = - FragmentHelpBinding.inflate(inflater, container, false) - return fragmentHelpBinding?.root - } - - private fun sendDiagnosticReport() { - requireActivity().start() - } - - override fun onDestroyView() { - super.onDestroyView() - fragmentHelpBinding?.root?.removeAllViews() - fragmentHelpBinding = null + ): View? = ComposeView(requireContext()).apply { + setContent { + // Create the helpScreen data using your rawTitleDescriptionMap. + val helpScreenData = transformToHelpScreenData( + requireContext(), + rawTitleDescriptionMap() + ) + // Call your HelpScreen composable. + HelpScreen(data = helpScreenData) { + NavigationIcon(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) + } + } + } +} + +// Util function to modify the data accordingly +fun transformToHelpScreenData( + context: Context, + rawTitleDescriptionMap: List> +): List { + return rawTitleDescriptionMap.map { (titleResId, description) -> + val title = context.getString(titleResId) + val descriptionValue = when (description) { + is String -> description + is Int -> context.resources.getStringArray(description).joinToString(separator = "\n") + else -> { + throw IllegalArgumentException("Invalid description resource type for title: $titleResId") + } + } + HelpScreenItemDataClass(title, descriptionValue) } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreen.kt new file mode 100644 index 000000000..0b06bde81 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreen.kt @@ -0,0 +1,124 @@ +/* + * 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.help + +import android.app.Activity +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize +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.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.error.DiagnosticReportActivity +import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.start +import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar +import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme +import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray350 +import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray600 +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.HELP_SCREEN_DIVIDER_HEIGHT +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP + +const val SEND_DIAGNOSTIC_REPORT_TESTING_TAG = "sendDiagnosticReportTestingTag" +const val HELP_SCREEN_ITEM_TITLE_TESTING_TAG = "helpScreenItemTitleTestingTag" +const val HELP_SCREEN_ITEM_DESCRIPTION_TESTING_TAG = "helpScreenItemDescriptionTestingTag" + +@OptIn(ExperimentalMaterial3Api::class) +@Suppress("ComposableLambdaParameterNaming") +@Composable +fun HelpScreen( + data: List, + navigationIcon: @Composable () -> Unit +) { + val dividerColor = if (isSystemInDarkTheme()) { + MineShaftGray600 + } else { + MineShaftGray350 + } + KiwixTheme { + Scaffold( + topBar = { + KiwixAppBar(R.string.menu_help, navigationIcon) + } + ) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding)) { + SendReportRow() + HorizontalDivider(color = dividerColor, thickness = HELP_SCREEN_DIVIDER_HEIGHT) + HelpItemList(data, dividerColor) + } + } + } +} + +@Composable +fun SendReportRow() { + val context = LocalContext.current + val isDarkTheme = isSystemInDarkTheme() + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { (context as? Activity)?.start() } + .testTag(SEND_DIAGNOSTIC_REPORT_TESTING_TAG), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Image( + painter = painterResource(R.drawable.ic_feedback_orange_24dp), + contentDescription = stringResource(R.string.send_report), + modifier = Modifier.padding(SIXTEEN_DP) + ) + + Text( + text = stringResource(R.string.send_report), + color = if (isDarkTheme) Color.LightGray else Color.DarkGray, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal), + modifier = Modifier.minimumInteractiveComponentSize() + ) + } +} + +@Composable +fun HelpItemList(data: List, dividerColor: Color) { + LazyColumn(modifier = Modifier.fillMaxWidth()) { + itemsIndexed(data, key = { _, item -> item.title }) { _, item -> + HelpScreenItem(data = item) + HorizontalDivider(color = dividerColor, thickness = HELP_SCREEN_DIVIDER_HEIGHT) + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItem.kt new file mode 100644 index 000000000..d2f921307 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItem.kt @@ -0,0 +1,178 @@ +/* + * 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.help + +import android.content.Context +import android.text.method.LinkMovementMethod +import android.text.util.Linkify +import android.view.Gravity +import android.widget.TextView +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +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.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.text.util.LinkifyCompat +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray900 +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.HELP_SCREEN_ARROW_ICON_SIZE +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.HELP_SCREEN_ITEM_TITLE_LETTER_SPACING +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.HELP_SCREEN_ITEM_TITLE_TEXT_SIZE +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP + +private const val HELP_ITEM_ANIMATION_DURATION = 300 +private const val HELP_ITEM_ARROW_ROTATION_OPEN = 180f +private const val HELP_ITEM_ARROW_ROTATION_CLOSE = 0f + +@Composable +fun HelpScreenItem( + modifier: Modifier = Modifier, + data: HelpScreenItemDataClass, + initiallyOpened: Boolean = false +) { + var isOpen by remember { mutableStateOf(initiallyOpened) } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(vertical = EIGHT_DP, horizontal = SIXTEEN_DP), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + HelpItemHeader(data.title, isOpen) { isOpen = !isOpen } + AnimatedVisibility(visible = isOpen) { + Spacer(modifier = Modifier.height(EIGHT_DP)) + HelpItemDescription(LocalContext.current, data.description) + } + } +} + +@Composable +fun HelpItemHeader( + title: String, + isOpen: Boolean, + onToggle: () -> Unit +) { + val arrowRotation by animateFloatAsState( + targetValue = if (isOpen) HELP_ITEM_ARROW_ROTATION_OPEN else HELP_ITEM_ARROW_ROTATION_CLOSE, + animationSpec = tween(HELP_ITEM_ANIMATION_DURATION), + label = "arrowRotation" + ) + val interactionSource = remember(::MutableInteractionSource) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable(interactionSource = interactionSource, indication = null, onClick = onToggle) + .testTag(HELP_SCREEN_ITEM_TITLE_TESTING_TAG) + ) { + Text( + text = title, + fontSize = HELP_SCREEN_ITEM_TITLE_TEXT_SIZE, + style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Medium), + letterSpacing = HELP_SCREEN_ITEM_TITLE_LETTER_SPACING, + modifier = Modifier.minimumInteractiveComponentSize() + ) + Image( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = stringResource(R.string.expand), + modifier = Modifier + .graphicsLayer { + rotationZ = arrowRotation + } + .defaultMinSize( + minWidth = HELP_SCREEN_ARROW_ICON_SIZE, + minHeight = HELP_SCREEN_ARROW_ICON_SIZE + ) + .minimumInteractiveComponentSize(), + colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface) + ) + } +} + +@Composable +fun HelpItemDescription(context: Context, description: String) { + val textColor = if (isSystemInDarkTheme()) { + Color.LightGray + } else { + MineShaftGray900 + } + val helpItemDescription = remember { TextView(context) } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = SIXTEEN_DP) + ) { + AndroidView( + factory = { helpItemDescription }, + modifier = Modifier.padding(bottom = SIXTEEN_DP) + .testTag(HELP_SCREEN_ITEM_DESCRIPTION_TESTING_TAG) + .semantics { contentDescription = description } + ) { textView -> + textView.apply { + text = description + setTextAppearance(R.style.TextAppearance_KiwixTheme_Subtitle2) + setTextColor(textColor.toArgb()) + minHeight = + context.resources.getDimensionPixelSize(R.dimen.material_minimum_height_and_width) + gravity = Gravity.CENTER or Gravity.START + LinkifyCompat.addLinks(this, Linkify.WEB_URLS) + movementMethod = LinkMovementMethod.getInstance() + } + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItemDataClass.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItemDataClass.kt new file mode 100644 index 000000000..9b4dd1358 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItemDataClass.kt @@ -0,0 +1,22 @@ +/* + * 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.help + +// Same as HelpItem data class in earlier in XML +data class HelpScreenItemDataClass(val title: String, val description: String) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CoreSettingsFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CoreSettingsFragment.kt index 579499e3f..6d258ec80 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CoreSettingsFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CoreSettingsFragment.kt @@ -59,6 +59,7 @@ abstract class CoreSettingsFragment : BaseFragment() { requireActivity().supportFragmentManager.beginTransaction().remove(prefsFragment) .commitNowAllowingStateLoss() super.onDestroyView() + settingsBinding?.root?.removeAllViews() settingsBinding = null } } 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 c4ac32a1f..d597ba518 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 @@ -98,4 +98,10 @@ object ComposeDimens { // LocalLibraryFragment dimens val FAB_ICON_BOTTOM_MARGIN = 50.dp + + // HelpFragment dimens + val HELP_SCREEN_DIVIDER_HEIGHT = 0.7.dp + val HELP_SCREEN_ITEM_TITLE_TEXT_SIZE = 20.sp + val HELP_SCREEN_ITEM_TITLE_LETTER_SPACING = 0.0125.em + val HELP_SCREEN_ARROW_ICON_SIZE = 35.dp } diff --git a/core/src/main/res/layout/fragment_help.xml b/core/src/main/res/layout/fragment_help.xml deleted file mode 100644 index e25e528db..000000000 --- a/core/src/main/res/layout/fragment_help.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - diff --git a/core/src/main/res/layout/fragment_page.xml b/core/src/main/res/layout/fragment_page.xml index d4e449bd2..2fb96ca8a 100644 --- a/core/src/main/res/layout/fragment_page.xml +++ b/core/src/main/res/layout/fragment_page.xml @@ -9,8 +9,7 @@ android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - app:layout_constraintTop_toTopOf="parent" - tools:showIn="@layout/fragment_help"> + app:layout_constraintTop_toTopOf="parent"> - - - - - - - - diff --git a/core/src/main/res/layout/layout_standard_app_bar.xml b/core/src/main/res/layout/layout_standard_app_bar.xml index 15e881b34..9cef6de5a 100644 --- a/core/src/main/res/layout/layout_standard_app_bar.xml +++ b/core/src/main/res/layout/layout_standard_app_bar.xml @@ -6,8 +6,7 @@ android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - app:layout_constraintTop_toTopOf="parent" - tools:showIn="@layout/fragment_help"> + app:layout_constraintTop_toTopOf="parent">