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.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import com.adevinta.android.barista.interaction.BaristaSleepInteractions import com.adevinta.android.barista.interaction.BaristaSleepInteractions
import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.StringId.TextId
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.help.SEND_DIAGNOSTIC_REPORT_TESTING_TAG
import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils
fun errorActivity(func: ErrorActivityRobot.() -> Unit) = ErrorActivityRobot().apply(func) fun errorActivity(func: ErrorActivityRobot.() -> Unit) = ErrorActivityRobot().apply(func)
class ErrorActivityRobot : BaseRobot() { class ErrorActivityRobot : BaseRobot() {
fun assertSendDiagnosticReportDisplayed() { fun assertSendDiagnosticReportDisplayed(composeTestRule: ComposeContentTestRule) {
// Wait a bit for properly visible the HelpFragment. // Wait a bit for properly visible the HelpFragment.
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong()) 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() { fun clickOnSendDiagnosticReport(composeTestRule: ComposeContentTestRule) {
clickOn(TextId(R.string.send_report)) composeTestRule.apply {
waitForIdle()
onNodeWithTag(SEND_DIAGNOSTIC_REPORT_TESTING_TAG).performClick()
}
} }
fun assertErrorActivityDisplayed(composeTestRule: ComposeContentTestRule) { fun assertErrorActivityDisplayed(composeTestRule: ComposeContentTestRule) {

View File

@ -91,8 +91,8 @@ class ErrorActivityTest : BaseActivityTest() {
it.navigate(R.id.helpFragment) it.navigate(R.id.helpFragment)
} }
errorActivity { errorActivity {
assertSendDiagnosticReportDisplayed() assertSendDiagnosticReportDisplayed(composeTestRule)
clickOnSendDiagnosticReport() clickOnSendDiagnosticReport(composeTestRule)
assertErrorActivityDisplayed(composeTestRule) assertErrorActivityDisplayed(composeTestRule)
// Click on "No, Thanks" button to see it's functionality working or not. // Click on "No, Thanks" button to see it's functionality working or not.
clickOnNoThanksButton(composeTestRule) clickOnNoThanksButton(composeTestRule)
@ -101,9 +101,9 @@ class ErrorActivityTest : BaseActivityTest() {
it.navigate(R.id.helpFragment) it.navigate(R.id.helpFragment)
} }
// Assert HelpFragment is visible or not after clicking on the "No, Thanks" button. // 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. // Again click on "Send diagnostic report" button to open the ErrorActivity.
clickOnSendDiagnosticReport() clickOnSendDiagnosticReport(composeTestRule)
assertErrorActivityDisplayed(composeTestRule) assertErrorActivityDisplayed(composeTestRule)
// Check check boxes are displayed or not. // Check check boxes are displayed or not.
assertCheckBoxesDisplayed(composeTestRule) assertCheckBoxesDisplayed(composeTestRule)

View File

@ -18,6 +18,7 @@
package org.kiwix.kiwixmobile.help package org.kiwix.kiwixmobile.help
import android.os.Build import android.os.Build
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
@ -33,6 +34,7 @@ import org.kiwix.kiwixmobile.BaseActivityTest
import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil 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.core.utils.TestingUtils.RETRY_RULE_ORDER
import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.testutils.RetryRule import org.kiwix.kiwixmobile.testutils.RetryRule
@ -43,6 +45,13 @@ import org.kiwix.kiwixmobile.utils.KiwixIdlingResource
class HelpFragmentTest : BaseActivityTest() { class HelpFragmentTest : BaseActivityTest() {
private lateinit var sharedPreferenceUtil: SharedPreferenceUtil 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 @Before
override fun waitForIdle() { override fun waitForIdle() {
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
@ -66,10 +75,6 @@ class HelpFragmentTest : BaseActivityTest() {
} }
} }
@Rule(order = RETRY_RULE_ORDER)
@JvmField
val retryRule = RetryRule()
init { init {
AccessibilityChecks.enable().setRunChecksFromRootView(true) AccessibilityChecks.enable().setRunChecksFromRootView(true)
} }
@ -81,16 +86,16 @@ class HelpFragmentTest : BaseActivityTest() {
it.navigate(R.id.helpFragment) it.navigate(R.id.helpFragment)
} }
help { help {
clickOnWhatDoesKiwixDo() clickOnWhatDoesKiwixDo(composeTestRule)
assertWhatDoesKiwixDoIsExpanded() assertWhatDoesKiwixDoIsExpanded(composeTestRule)
clickOnWhatDoesKiwixDo() clickOnWhatDoesKiwixDo(composeTestRule)
clickOnWhereIsContent() clickOnWhereIsContent(composeTestRule)
assertWhereIsContentIsExpanded() assertWhereIsContentIsExpanded(composeTestRule)
clickOnWhereIsContent() clickOnWhereIsContent(composeTestRule)
clickOnHowToUpdateContent() clickOnHowToUpdateContent(composeTestRule)
assertHowToUpdateContentIsExpanded() assertHowToUpdateContentIsExpanded(composeTestRule)
clickOnHowToUpdateContent() clickOnHowToUpdateContent(composeTestRule)
assertWhyCopyMoveFilesToAppPublicDirectoryIsNotVisible() assertWhyCopyMoveFilesToAppPublicDirectoryIsNotVisible(composeTestRule)
} }
LeakAssertions.assertNoLeaks() LeakAssertions.assertNoLeaks()
} }
@ -103,18 +108,18 @@ class HelpFragmentTest : BaseActivityTest() {
it.navigate(R.id.helpFragment) it.navigate(R.id.helpFragment)
} }
help { help {
clickOnWhatDoesKiwixDo() clickOnWhatDoesKiwixDo(composeTestRule)
assertWhatDoesKiwixDoIsExpanded() assertWhatDoesKiwixDoIsExpanded(composeTestRule)
clickOnWhatDoesKiwixDo() clickOnWhatDoesKiwixDo(composeTestRule)
clickOnWhereIsContent() clickOnWhereIsContent(composeTestRule)
assertWhereIsContentIsExpanded() assertWhereIsContentIsExpanded(composeTestRule)
clickOnWhereIsContent() clickOnWhereIsContent(composeTestRule)
clickOnHowToUpdateContent() clickOnHowToUpdateContent(composeTestRule)
assertHowToUpdateContentIsExpanded() assertHowToUpdateContentIsExpanded(composeTestRule)
clickOnHowToUpdateContent() clickOnHowToUpdateContent(composeTestRule)
clickWhyCopyMoveFilesToAppPublicDirectory() clickWhyCopyMoveFilesToAppPublicDirectory(composeTestRule)
assertWhyCopyMoveFilesToAppPublicDirectoryIsExpanded() assertWhyCopyMoveFilesToAppPublicDirectoryIsExpanded(composeTestRule)
clickWhyCopyMoveFilesToAppPublicDirectory() clickWhyCopyMoveFilesToAppPublicDirectory(composeTestRule)
} }
LeakAssertions.assertNoLeaks() LeakAssertions.assertNoLeaks()
} }

View File

@ -17,80 +17,128 @@
*/ */
package org.kiwix.kiwixmobile.help 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.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.StringId.TextId import org.kiwix.kiwixmobile.core.R
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.string 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 import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
fun help(func: HelpRobot.() -> Unit) = HelpRobot().apply(func) fun help(func: HelpRobot.() -> Unit) = HelpRobot().apply(func)
class HelpRobot : BaseRobot() { class HelpRobot : BaseRobot() {
fun assertToolbarDisplayed() { fun assertToolbarDisplayed(composeTestRule: ComposeContentTestRule) {
isVisible(ViewId(id.toolbar)) composeTestRule.apply {
waitForIdle()
onNodeWithTag(TOOLBAR_TITLE_TESTING_TAG)
.assertTextEquals(context.getString(R.string.menu_help))
}
} }
fun clickOnWhatDoesKiwixDo() { fun clickOnWhatDoesKiwixDo(composeTestRule: ComposeContentTestRule) {
testFlakyView({ onView(withText(string.help_2)).perform(click()) }) clickOnHelpScreenItemTitle(0, composeTestRule)
} }
fun assertWhatDoesKiwixDoIsExpanded() { fun assertWhatDoesKiwixDoIsExpanded(composeTestRule: ComposeContentTestRule) {
isVisible( assertHelpScreenDescriptionDisplayed(
Text( helpTextFormat(string.help_3, string.help_4),
helpTextFormat( composeTestRule
string.help_3,
string.help_4
)
)
) )
} }
fun clickOnWhereIsContent() { fun clickOnWhereIsContent(composeTestRule: ComposeContentTestRule) {
clickOn(TextId(string.help_5)) clickOnHelpScreenItemTitle(1, composeTestRule)
} }
fun assertWhereIsContentIsExpanded() { fun assertWhereIsContentIsExpanded(composeTestRule: ComposeContentTestRule) {
isVisible( assertHelpScreenDescriptionDisplayed(
Text( helpTextFormat(
helpTextFormat( string.help_6,
string.help_6, string.help_7,
string.help_7, string.help_8,
string.help_8, string.help_9,
string.help_9, string.help_10,
string.help_10, string.help_11
string.help_11 ),
) composeTestRule
)
) )
} }
fun clickOnHowToUpdateContent() { fun clickOnHowToUpdateContent(composeTestRule: ComposeContentTestRule) {
clickOn(TextId(string.how_to_update_content)) clickOnHelpScreenItemTitle(2, composeTestRule)
} }
fun assertHowToUpdateContentIsExpanded() { fun assertHowToUpdateContentIsExpanded(composeTestRule: ComposeContentTestRule) {
isVisible(TextId(string.update_content_description)) assertHelpScreenDescriptionDisplayed(
context.getString(string.update_content_description),
composeTestRule
)
} }
fun clickWhyCopyMoveFilesToAppPublicDirectory() { fun clickWhyCopyMoveFilesToAppPublicDirectory(composeTestRule: ComposeContentTestRule) {
clickOn(TextId(string.why_copy_move_files_to_app_directory)) clickOnHelpScreenItemTitle(3, composeTestRule)
} }
fun assertWhyCopyMoveFilesToAppPublicDirectoryIsExpanded() { fun assertWhyCopyMoveFilesToAppPublicDirectoryIsExpanded(composeTestRule: ComposeContentTestRule) {
isVisible(Text(context.getString(string.copy_move_files_to_app_directory_description))) 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)) onView(withText(string.why_copy_move_files_to_app_directory))
.check(doesNotExist()) .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) = private fun helpTextFormat(vararg stringIds: Int) =
stringIds.joinToString(separator = "\n", transform = context::getString) stringIds.joinToString(separator = "\n", transform = context::getString)
} }

View File

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

View File

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

View File

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

View File

@ -20,7 +20,6 @@ package plugin
import Config import Config
import Libs import Libs
import Versions
import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.CommonExtension
import com.android.build.gradle.BaseExtension import com.android.build.gradle.BaseExtension
import io.gitlab.arturbosch.detekt.extensions.DetektExtension 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 package org.kiwix.kiwixmobile.core.help
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.compose.ui.platform.ComposeView
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.DividerItemDecoration
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.BaseFragment 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.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import javax.inject.Inject import javax.inject.Inject
@ -38,64 +34,47 @@ import javax.inject.Inject
abstract class HelpFragment : BaseFragment() { abstract class HelpFragment : BaseFragment() {
@Inject @Inject
lateinit var sharedPreferenceUtil: SharedPreferenceUtil 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() 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) { override fun inject(baseActivity: BaseActivity) {
(baseActivity as CoreMainActivity).cachedComponent.inject(this) (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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? = ComposeView(requireContext()).apply {
fragmentHelpBinding = setContent {
FragmentHelpBinding.inflate(inflater, container, false) // Create the helpScreen data using your rawTitleDescriptionMap.
return fragmentHelpBinding?.root val helpScreenData = transformToHelpScreenData(
} requireContext(),
rawTitleDescriptionMap()
private fun sendDiagnosticReport() { )
requireActivity().start<DiagnosticReportActivity>() // Call your HelpScreen composable.
} HelpScreen(data = helpScreenData) {
NavigationIcon(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() })
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) requireActivity().supportFragmentManager.beginTransaction().remove(prefsFragment)
.commitNowAllowingStateLoss() .commitNowAllowingStateLoss()
super.onDestroyView() super.onDestroyView()
settingsBinding?.root?.removeAllViews()
settingsBinding = null settingsBinding = null
} }
} }

View File

@ -98,4 +98,10 @@ object ComposeDimens {
// LocalLibraryFragment dimens // LocalLibraryFragment dimens
val FAB_ICON_BOTTOM_MARGIN = 50.dp 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:id="@+id/app_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent">
tools:showIn="@layout/fragment_help">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/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:id="@+id/app_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent">
tools:showIn="@layout/fragment_help">
<include layout="@layout/layout_toolbar" /> <include layout="@layout/layout_toolbar" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>