Merge pull request #4217 from SOUMEN-PAL/4159-migrate-help-fragment-to-compose

4159 migrate help fragment to compose
This commit is contained in:
Kelson 2025-03-28 16:02:31 +01:00 committed by GitHub
commit eeba4efdc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 506 additions and 318 deletions

View File

@ -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) {

View File

@ -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)

View File

@ -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()
}

View File

@ -17,47 +17,51 @@
*/
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(
fun assertWhereIsContentIsExpanded(composeTestRule: ComposeContentTestRule) {
assertHelpScreenDescriptionDisplayed(
helpTextFormat(
string.help_6,
string.help_7,
@ -65,32 +69,76 @@ class HelpRobot : BaseRobot() {
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)
}

View File

@ -130,7 +130,7 @@ class TopLevelDestinationTest : BaseActivityTest() {
}
clickHostBooksOnSideNav(ZimHostRobot::assertMenuWifiHotspotDiplayed)
clickSettingsOnSideNav(SettingsRobot::assertMenuSettingsDisplayed)
clickHelpOnSideNav(HelpRobot::assertToolbarDisplayed)
clickHelpOnSideNav { HelpRobot().assertToolbarDisplayed(composeTestRule) }
clickSupportKiwixOnSideNav()
pressBack()
}

View File

@ -134,7 +134,7 @@ class GetContentShortcutTest {
}
clickHostBooksOnSideNav(ZimHostRobot::assertMenuWifiHotspotDiplayed)
clickSettingsOnSideNav(SettingsRobot::assertMenuSettingsDisplayed)
clickHelpOnSideNav(HelpRobot::assertToolbarDisplayed)
clickHelpOnSideNav { HelpRobot().assertToolbarDisplayed(composeTestRule) }
clickSupportKiwixOnSideNav()
pressBack()
}

View File

@ -129,8 +129,7 @@
<fragment
android:id="@+id/helpFragment"
android:name="org.kiwix.kiwixmobile.help.KiwixHelpFragment"
android:label="HelpFragment"
tools:layout="@layout/fragment_help" />
android:label="HelpFragment" />
<fragment
android:id="@+id/kiwixSettingsFragment"
android:name="org.kiwix.kiwixmobile.settings.KiwixSettingsFragment"

View File

@ -20,7 +20,6 @@ package plugin
import Config
import Libs
import Versions
import com.android.build.api.dsl.CommonExtension
import com.android.build.gradle.BaseExtension
import io.gitlab.arturbosch.detekt.extensions.DetektExtension

View File

@ -1,74 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<String, String>) :
RecyclerView.Adapter<HelpAdapter.Item>() {
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<HelpItem>(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)

View File

@ -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<Pair<Int, Any>> = 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
): 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() })
}
}
}
}
private fun sendDiagnosticReport() {
requireActivity().start<DiagnosticReportActivity>()
}
override fun onDestroyView() {
super.onDestroyView()
fragmentHelpBinding?.root?.removeAllViews()
fragmentHelpBinding = null
// Util function to modify the data accordingly
fun transformToHelpScreenData(
context: Context,
rawTitleDescriptionMap: List<Pair<Int, Any>>
): List<HelpScreenItemDataClass> {
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)
}
}

View File

@ -0,0 +1,124 @@
/*
* Kiwix Android
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<HelpScreenItemDataClass>,
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<DiagnosticReportActivity>() }
.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<HelpScreenItemDataClass>, dividerColor: Color) {
LazyColumn(modifier = Modifier.fillMaxWidth()) {
itemsIndexed(data, key = { _, item -> item.title }) { _, item ->
HelpScreenItem(data = item)
HorizontalDivider(color = dividerColor, thickness = HELP_SCREEN_DIVIDER_HEIGHT)
}
}
}

View File

@ -0,0 +1,178 @@
/*
* Kiwix Android
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
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()
}
}
}
}

View File

@ -0,0 +1,22 @@
/*
* Kiwix Android
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.help
// Same as HelpItem data class in earlier in XML
data class HelpScreenItemDataClass(val title: String, val description: String)

View File

@ -59,6 +59,7 @@ abstract class CoreSettingsFragment : BaseFragment() {
requireActivity().supportFragmentManager.beginTransaction().remove(prefsFragment)
.commitNowAllowingStateLoss()
super.onDestroyView()
settingsBinding?.root?.removeAllViews()
settingsBinding = null
}
}

View File

@ -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
}

View File

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".help.HelpFragment">
<include layout="@layout/layout_standard_app_bar" />
<ImageView
android:id="@+id/activity_help_diagnostic_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/send_report"
android:padding="@dimen/activity_horizontal_margin"
android:src="@drawable/ic_feedback_orange_24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/app_bar" />
<TextView
android:id="@+id/activity_help_diagnostic_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawablePadding="@dimen/activity_horizontal_margin"
android:gravity="start|center"
android:minHeight="@dimen/material_minimum_height_and_width"
android:text="@string/send_report"
android:textAppearance="?android:attr/textAppearanceMedium"
app:layout_constraintBottom_toBottomOf="@id/activity_help_diagnostic_image_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/activity_help_diagnostic_image_view"
app:layout_constraintTop_toTopOf="@id/activity_help_diagnostic_image_view" />
<View
android:id="@+id/activity_help_diagnostic_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
app:layout_constraintTop_toBottomOf="@id/activity_help_diagnostic_image_view" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/activity_help_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/activity_help_diagnostic_divider"
tools:listitem="@layout/item_help" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/dimen_medium_padding"
android:paddingEnd="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/dimen_medium_padding">
<TextView
android:id="@+id/item_help_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="start|center"
android:minHeight="@dimen/material_minimum_height_and_width"
android:textAppearance="?textAppearanceHeadline6"
app:layout_constraintEnd_toStartOf="@id/item_help_toggle_expand"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/help_2" />
<ImageView
android:id="@+id/item_help_toggle_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/expand"
android:minWidth="@dimen/material_minimum_height_and_width"
android:minHeight="@dimen/material_minimum_height_and_width"
android:scaleType="centerInside"
android:src="@drawable/action_find_next"
app:layout_constraintBottom_toBottomOf="@id/item_help_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/item_help_title"
app:tint="?colorOnSurface" />
<TextView
android:id="@+id/item_help_description"
style="@style/list_item_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:paddingTop="@dimen/activity_vertical_margin"
android:textColor="?textSecondary"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/item_help_title"
tools:text="@string/help_3"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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">
<include layout="@layout/layout_toolbar" />
</com.google.android.material.appbar.AppBarLayout>