Merge pull request #3218 from kiwix/Issue#2537

Fixed Hosted Books don't update on Application
This commit is contained in:
Kelson 2023-11-12 18:00:20 +01:00 committed by GitHub
commit f53809abc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 538 additions and 101 deletions

View File

@ -20,7 +20,6 @@ package org.kiwix.kiwixmobile
import android.Manifest.permission import android.Manifest.permission
import android.content.Context import android.content.Context
import android.os.Build
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
@ -35,18 +34,10 @@ import org.kiwix.kiwixmobile.main.KiwixMainActivity
abstract class BaseActivityTest { abstract class BaseActivityTest {
open lateinit var activityScenario: ActivityScenario<KiwixMainActivity> open lateinit var activityScenario: ActivityScenario<KiwixMainActivity>
private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { private val permissions = arrayOf(
arrayOf(
permission.READ_EXTERNAL_STORAGE,
permission.WRITE_EXTERNAL_STORAGE,
permission.SYSTEM_ALERT_WINDOW
)
} else {
arrayOf(
permission.READ_EXTERNAL_STORAGE, permission.READ_EXTERNAL_STORAGE,
permission.WRITE_EXTERNAL_STORAGE permission.WRITE_EXTERNAL_STORAGE
) )
}
@get:Rule @get:Rule
var permissionRules: GrantPermissionRule = var permissionRules: GrantPermissionRule =

View File

@ -18,16 +18,15 @@
package org.kiwix.kiwixmobile package org.kiwix.kiwixmobile
import android.Manifest import android.Manifest
import android.os.Build
import android.util.Log import android.util.Log
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingPolicies import androidx.test.espresso.IdlingPolicies
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule import androidx.test.rule.GrantPermissionRule
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import com.adevinta.android.barista.interaction.BaristaDialogInteractions import com.adevinta.android.barista.interaction.BaristaDialogInteractions
@ -58,18 +57,10 @@ class NetworkTest {
// @Inject // @Inject
// MockWebServer mockWebServer // MockWebServer mockWebServer
private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { private val permissions = arrayOf(
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.SYSTEM_ALERT_WINDOW
)
} else {
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) )
}
@Rule @Rule
@JvmField @JvmField

View File

@ -19,7 +19,6 @@ package org.kiwix.kiwixmobile.language
import android.Manifest import android.Manifest
import android.app.Instrumentation import android.app.Instrumentation
import android.os.Build
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
@ -50,18 +49,10 @@ class LanguageFragmentTest {
@get:Rule @get:Rule
var activityScenarioRule = ActivityScenarioRule(KiwixMainActivity::class.java) var activityScenarioRule = ActivityScenarioRule(KiwixMainActivity::class.java)
private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { private val permissions = arrayOf(
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.SYSTEM_ALERT_WINDOW
)
} else {
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) )
}
@Rule @Rule
@JvmField @JvmField

View File

@ -19,20 +19,25 @@
package org.kiwix.kiwixmobile.nav.destination.library package org.kiwix.kiwixmobile.nav.destination.library
import android.util.Log import android.util.Log
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import applyWithViewHierarchyPrinting import applyWithViewHierarchyPrinting
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.Text
import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.Findable.ViewId
import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferRobot import org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferRobot
import org.kiwix.kiwixmobile.localFileTransfer.localFileTransfer import org.kiwix.kiwixmobile.localFileTransfer.localFileTransfer
import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils
import org.kiwix.kiwixmobile.utils.RecyclerViewItemCount
fun library(func: LibraryRobot.() -> Unit) = LibraryRobot().applyWithViewHierarchyPrinting(func) fun library(func: LibraryRobot.() -> Unit) = LibraryRobot().applyWithViewHierarchyPrinting(func)
@ -60,7 +65,17 @@ class LibraryRobot : BaseRobot() {
fun deleteZimIfExists() { fun deleteZimIfExists() {
try { try {
longClickOnZimFile() val recyclerViewId: Int = R.id.zimfilelist
val recyclerViewItemsCount = RecyclerViewItemCount(recyclerViewId).checkRecyclerViewCount()
// Scroll to the end of the RecyclerView to ensure all items are visible
onView(withId(recyclerViewId))
.perform(scrollToPosition<ViewHolder>(recyclerViewItemsCount - 1))
for (position in 0 until recyclerViewItemsCount) {
// Long-click the item to select it
onView(withId(recyclerViewId))
.perform(actionOnItemAtPosition<ViewHolder>(position, longClick()))
}
clickOnFileDeleteIcon() clickOnFileDeleteIcon()
assertDeleteDialogDisplayed() assertDeleteDialogDisplayed()
clickOnDeleteZimFile() clickOnDeleteZimFile()
@ -75,6 +90,7 @@ class LibraryRobot : BaseRobot() {
} }
private fun clickOnFileDeleteIcon() { private fun clickOnFileDeleteIcon() {
pauseForBetterTestPerformance()
clickOn(ViewId(R.id.zim_file_delete_item)) clickOn(ViewId(R.id.zim_file_delete_item))
} }
@ -84,10 +100,6 @@ class LibraryRobot : BaseRobot() {
.check(ViewAssertions.matches(isDisplayed())) .check(ViewAssertions.matches(isDisplayed()))
} }
private fun longClickOnZimFile() {
longClickOn(Text(zimFileTitle))
}
private fun clickOnDeleteZimFile() { private fun clickOnDeleteZimFile() {
pauseForBetterTestPerformance() pauseForBetterTestPerformance()
onView(withText("DELETE")).perform(click()) onView(withText("DELETE")).perform(click())

View File

@ -18,7 +18,6 @@
package org.kiwix.kiwixmobile.settings package org.kiwix.kiwixmobile.settings
import android.Manifest import android.Manifest
import android.os.Build
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.internal.runner.junit4.statement.UiThreadStatement import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
@ -46,18 +45,10 @@ class KiwixSettingsFragmentTest {
@get:Rule @get:Rule
var activityScenarioRule = ActivityScenarioRule(KiwixMainActivity::class.java) var activityScenarioRule = ActivityScenarioRule(KiwixMainActivity::class.java)
private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { private val permissions = arrayOf(
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.SYSTEM_ALERT_WINDOW
)
} else {
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) )
}
@Rule @Rule
@JvmField @JvmField

View File

@ -19,7 +19,6 @@ package org.kiwix.kiwixmobile.splash
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.os.Build
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
@ -59,18 +58,10 @@ class KiwixSplashActivityTest {
private val activityScenario: ActivityScenario<KiwixMainActivity> = private val activityScenario: ActivityScenario<KiwixMainActivity> =
ActivityScenario.launch(KiwixMainActivity::class.java) ActivityScenario.launch(KiwixMainActivity::class.java)
private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { private val permissions = arrayOf(
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.SYSTEM_ALERT_WINDOW
)
} else {
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) )
}
@Rule @Rule
@JvmField @JvmField

View File

@ -0,0 +1,41 @@
/*
* Kiwix Android
* Copyright (c) 2023 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.utils
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.matcher.ViewMatchers.withId
class RecyclerViewItemCount(private val recyclerViewId: Int) {
fun checkRecyclerViewCount(): Int {
var recyclerViewItemCount = 0
onView(withId(recyclerViewId))
.check { view: View, noViewFoundException: NoMatchingViewException? ->
if (noViewFoundException != null) {
throw noViewFoundException
}
val recyclerView = view as RecyclerView
// Get the item count from the RecyclerView
recyclerViewItemCount = recyclerView.adapter?.itemCount ?: 0
}
return recyclerViewItemCount
}
}

View File

@ -0,0 +1,75 @@
/*
* Kiwix Android
* Copyright (c) 2023 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.utils
import android.view.View
import android.widget.CheckBox
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import org.hamcrest.core.AllOf.allOf
class RecyclerViewSelectedCheckBoxCountAssertion(
private val recyclerViewId: Int,
private val checkBoxId: Int
) {
fun countCheckedCheckboxes(): Int {
var checkedCount = 0
// Find the RecyclerView
val recyclerViewMatcher: Matcher<View> = allOf(
isAssignableFrom(RecyclerView::class.java),
isDisplayed(),
withId(recyclerViewId)
)
// Use a custom ViewMatcher to find checkboxes that are checked
val checkBoxMatcher: Matcher<View> = object : TypeSafeMatcher<View>() {
override fun matchesSafely(view: View): Boolean =
view is CheckBox && view.isChecked
override fun describeTo(description: Description) {
description.appendText("is checked")
}
}
// Count the checked checkboxes
onView(recyclerViewMatcher).check { view, noViewFoundException ->
if (noViewFoundException != null) {
throw noViewFoundException
}
val recyclerView = view as RecyclerView
(0 until recyclerView.childCount)
.asSequence()
.map {
// Check the checkbox directly without using inRoot
recyclerView.getChildAt(it).findViewById<CheckBox>(checkBoxId)
}
.filter { it != null && checkBoxMatcher.matches(it) }
.forEach { _ -> checkedCount++ }
}
return checkedCount
}
}

View File

@ -0,0 +1,175 @@
/*
* Kiwix Android
* Copyright (c) 2023 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.webserver
import android.Manifest
import android.content.Context
import android.os.Build
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import leakcanary.LeakAssertions
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.testutils.RetryRule
import org.kiwix.kiwixmobile.testutils.TestUtils
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
class ZimHostFragmentTest {
@Rule
@JvmField
var retryRule = RetryRule()
private lateinit var sharedPreferenceUtil: SharedPreferenceUtil
private lateinit var activityScenario: ActivityScenario<KiwixMainActivity>
private val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.NEARBY_WIFI_DEVICES
)
} else {
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
)
}
@Rule
@JvmField
var permissionRules: GrantPermissionRule =
GrantPermissionRule.grant(*permissions)
private var context: Context? = null
@Before
fun waitForIdle() {
context = InstrumentationRegistry.getInstrumentation().targetContext
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
if (TestUtils.isSystemUINotRespondingDialogVisible(this)) {
TestUtils.closeSystemDialogs(context)
}
waitForIdle()
}
context?.let {
sharedPreferenceUtil = SharedPreferenceUtil(it).apply {
setIntroShown()
putPrefWifiOnly(false)
setIsPlayStoreBuildType(true)
prefIsTest = true
}
}
activityScenario = ActivityScenario.launch(KiwixMainActivity::class.java).apply {
moveToState(Lifecycle.State.RESUMED)
}
}
@Test
fun testZimHostFragment() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
activityScenario.onActivity {
it.navigate(R.id.libraryFragment)
}
loadZimFileInApplication("testzim.zim")
loadZimFileInApplication("small.zim")
zimHost {
refreshLibraryList()
assertZimFilesLoaded()
openZimHostFragment()
// Check if server is already started
stopServerIfAlreadyStarted()
// Check if both zim file are selected or not to properly run our test case
selectZimFileIfNotAlreadySelected()
clickOnTestZim()
// Start the server with one ZIM file
startServer()
assertServerStarted()
// Check that only one ZIM file is hosted on the server
assertItemHostedOnServer(1)
// Stop the server
stopServer()
assertServerStopped()
// Select the test ZIM file to host on the server
clickOnTestZim()
// Start the server with two ZIM files
startServer()
assertServerStarted()
// Check that both ZIM files are hosted on the server
assertItemHostedOnServer(2)
// Unselect the test ZIM to test restarting server functionality
clickOnTestZim()
// Check if the server is running
assertServerStarted()
// Check that only one ZIM file is hosted on the server after unselecting
assertItemHostedOnServer(1)
}
LeakAssertions.assertNoLeaks()
}
}
private fun loadZimFileInApplication(zimFileName: String) {
val loadFileStream =
ZimHostFragmentTest::class.java.classLoader.getResourceAsStream(zimFileName)
val zimFile = File(sharedPreferenceUtil.prefStorage, zimFileName)
if (zimFile.exists()) zimFile.delete()
zimFile.createNewFile()
loadFileStream.use { inputStream ->
val outputStream: OutputStream = FileOutputStream(zimFile)
outputStream.use { it ->
val buffer = ByteArray(inputStream.available())
var length: Int
while (inputStream.read(buffer).also { length = it } > 0) {
it.write(buffer, 0, length)
}
}
}
}
@After
fun setIsTestPreference() {
sharedPreferenceUtil.apply {
setIsPlayStoreBuildType(false)
prefIsTest = false
}
}
}

View File

@ -18,10 +18,27 @@
package org.kiwix.kiwixmobile.webserver package org.kiwix.kiwixmobile.webserver
import android.util.Log
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import applyWithViewHierarchyPrinting import applyWithViewHierarchyPrinting
import com.adevinta.android.barista.interaction.BaristaSleepInteractions
import com.adevinta.android.barista.interaction.BaristaSwipeRefreshInteractions.refresh
import junit.framework.AssertionFailedError
import org.hamcrest.CoreMatchers
import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.StringId.TextId import org.kiwix.kiwixmobile.Findable.StringId.TextId
import org.kiwix.kiwixmobile.Findable.Text
import org.kiwix.kiwixmobile.Findable.ViewId
import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.testutils.TestUtils
import org.kiwix.kiwixmobile.utils.RecyclerViewItemCount
import org.kiwix.kiwixmobile.utils.RecyclerViewMatcher
import org.kiwix.kiwixmobile.utils.RecyclerViewSelectedCheckBoxCountAssertion
import org.kiwix.kiwixmobile.utils.StandardActions.openDrawer
fun zimHost(func: ZimHostRobot.() -> Unit) = ZimHostRobot().applyWithViewHierarchyPrinting(func) fun zimHost(func: ZimHostRobot.() -> Unit) = ZimHostRobot().applyWithViewHierarchyPrinting(func)
@ -30,4 +47,107 @@ class ZimHostRobot : BaseRobot() {
fun assertMenuWifiHotspotDiplayed() { fun assertMenuWifiHotspotDiplayed() {
isVisible(TextId(R.string.menu_wifi_hotspot)) isVisible(TextId(R.string.menu_wifi_hotspot))
} }
fun refreshLibraryList() {
pauseForBetterTestPerformance()
refresh(R.id.zim_swiperefresh)
}
fun assertZimFilesLoaded() {
pauseForBetterTestPerformance()
isVisible(Text("Test_Zim"))
}
fun openZimHostFragment() {
openDrawer()
clickOn(TextId(R.string.menu_wifi_hotspot))
}
fun clickOnTestZim() {
clickOn(Text("Test_Zim"))
}
fun startServer() {
clickOn(ViewId(R.id.startServerButton))
pauseForBetterTestPerformance()
isVisible(TextId(R.string.wifi_dialog_title))
clickOn(TextId(R.string.hotspot_dialog_neutral_button))
}
fun assertServerStarted() {
pauseForBetterTestPerformance()
isVisible(Text("STOP SERVER"))
}
fun stopServerIfAlreadyStarted() {
try {
assertServerStarted()
stopServer()
} catch (exception: Exception) {
Log.i(
"ZIM_HOST_FRAGMENT",
"Failed to stop the server, Probably because server is not running"
)
}
}
fun selectZimFileIfNotAlreadySelected() {
try {
// check both files are selected.
assertItemHostedOnServer(2)
} catch (assertionFailedError: AssertionFailedError) {
try {
val recyclerViewItemsCount =
RecyclerViewItemCount(R.id.recyclerViewZimHost).checkRecyclerViewCount()
(0 until recyclerViewItemsCount)
.asSequence()
.filter { it != 0 }
.forEach(::selectZimFile)
} catch (assertionFailedError: AssertionFailedError) {
Log.i("ZIM_HOST_FRAGMENT", "Failed to select the zim file, probably it is already selected")
}
}
}
private fun selectZimFile(position: Int) {
pauseForBetterTestPerformance()
try {
onView(
RecyclerViewMatcher(R.id.recyclerViewZimHost).atPositionOnView(
position,
R.id.itemBookCheckbox
)
).check(matches(ViewMatchers.isChecked()))
} catch (assertionError: AssertionFailedError) {
pauseForBetterTestPerformance()
onView(
RecyclerViewMatcher(R.id.recyclerViewZimHost).atPositionOnView(
position,
R.id.itemBookCheckbox
)
).perform(click())
}
}
fun assertItemHostedOnServer(itemCount: Int) {
val checkedCheckboxCount =
RecyclerViewSelectedCheckBoxCountAssertion(
R.id.recyclerViewZimHost,
R.id.itemBookCheckbox
).countCheckedCheckboxes()
assertThat(checkedCheckboxCount, CoreMatchers.`is`(itemCount))
}
fun stopServer() {
clickOn(ViewId(R.id.startServerButton))
}
fun assertServerStopped() {
pauseForBetterTestPerformance()
isVisible(Text("START SERVER"))
}
private fun pauseForBetterTestPerformance() {
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong())
}
} }

Binary file not shown.

View File

@ -333,6 +333,7 @@ abstract class CoreReaderFragment :
private var readAloudService: ReadAloudService? = null private var readAloudService: ReadAloudService? = null
private var navigationHistoryList: MutableList<NavigationHistoryListItem> = ArrayList() private var navigationHistoryList: MutableList<NavigationHistoryListItem> = ArrayList()
private var isReadSelection = false private var isReadSelection = false
private var isReadAloudServiceRunning = false
private var storagePermissionForNotesLauncher: ActivityResultLauncher<String>? = private var storagePermissionForNotesLauncher: ActivityResultLauncher<String>? =
registerForActivityResult( registerForActivityResult(
@ -1047,8 +1048,7 @@ abstract class CoreReaderFragment :
} catch (ignore: IllegalArgumentException) { } catch (ignore: IllegalArgumentException) {
// to handle if service is already unbounded // to handle if service is already unbounded
} }
readAloudService?.registerCallBack(null) unRegisterReadAloudService()
readAloudService = null
storagePermissionForNotesLauncher?.unregister() storagePermissionForNotesLauncher?.unregister()
storagePermissionForNotesLauncher = null storagePermissionForNotesLauncher = null
} }
@ -2074,8 +2074,16 @@ abstract class CoreReaderFragment :
private fun unbindService() { private fun unbindService() {
readAloudService?.let { readAloudService?.let {
requireActivity().unbindService(serviceConnection) requireActivity().unbindService(serviceConnection)
if (!isReadAloudServiceRunning) {
unRegisterReadAloudService()
} }
} }
}
private fun unRegisterReadAloudService() {
readAloudService?.registerCallBack(null)
readAloudService = null
}
private fun createReadAloudIntent(action: String, isPauseTTS: Boolean): Intent = private fun createReadAloudIntent(action: String, isPauseTTS: Boolean): Intent =
Intent(requireActivity(), ReadAloudService::class.java).apply { Intent(requireActivity(), ReadAloudService::class.java).apply {
@ -2086,7 +2094,11 @@ abstract class CoreReaderFragment :
} }
private fun setActionAndStartTTSService(action: String, isPauseTTS: Boolean = false) { private fun setActionAndStartTTSService(action: String, isPauseTTS: Boolean = false) {
requireActivity().startService(createReadAloudIntent(action, isPauseTTS)) requireActivity().startService(
createReadAloudIntent(action, isPauseTTS)
).also {
isReadAloudServiceRunning = action == ACTION_PAUSE_OR_RESUME_TTS
}
} }
protected abstract fun restoreViewStateOnValidJSON( protected abstract fun restoreViewStateOnValidJSON(

View File

@ -60,8 +60,11 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
val prefIsFirstRun: Boolean val prefIsFirstRun: Boolean
get() = sharedPreferences.getBoolean(PREF_IS_FIRST_RUN, true) get() = sharedPreferences.getBoolean(PREF_IS_FIRST_RUN, true)
val prefIsTest: Boolean var prefIsTest: Boolean
get() = sharedPreferences.getBoolean(PREF_IS_TEST, false) get() = sharedPreferences.getBoolean(PREF_IS_TEST, false)
set(prefIsTest) {
sharedPreferences.edit { putBoolean(PREF_IS_TEST, prefIsTest) }
}
val prefShowShowCaseToUser: Boolean val prefShowShowCaseToUser: Boolean
get() = sharedPreferences.getBoolean(PREF_SHOW_SHOWCASE, true) get() = sharedPreferences.getBoolean(PREF_SHOW_SHOWCASE, true)

View File

@ -45,12 +45,15 @@ class WebServerHelper @Inject constructor(
private var isServerStarted = false private var isServerStarted = false
private var validIpAddressDisposable: Disposable? = null private var validIpAddressDisposable: Disposable? = null
fun startServerHelper(selectedBooksPath: ArrayList<String>): ServerStatus { fun startServerHelper(
selectedBooksPath: ArrayList<String>,
restartServer: Boolean
): ServerStatus? {
val ip = getIpAddress() val ip = getIpAddress()
return if (ip.isNullOrEmpty()) { return if (ip.isNullOrEmpty()) {
ServerStatus(false, R.string.error_ip_address_not_found) ServerStatus(false, R.string.error_ip_address_not_found)
} else { } else {
startAndroidWebServer(selectedBooksPath) startAndroidWebServer(selectedBooksPath, restartServer)
} }
} }
@ -61,9 +64,22 @@ class WebServerHelper @Inject constructor(
} }
} }
private fun startAndroidWebServer(selectedBooksPath: ArrayList<String>): ServerStatus { private fun startAndroidWebServer(
var errorMessage: Int? = null selectedBooksPath: ArrayList<String>,
restartServer: Boolean
): ServerStatus? {
var serverStatus: ServerStatus? = null
if (!isServerStarted) { if (!isServerStarted) {
serverStatus = startKiwixServer(selectedBooksPath)
} else if (restartServer) {
kiwixServer?.stopServer()
serverStatus = startKiwixServer(selectedBooksPath)
}
return serverStatus
}
private fun startKiwixServer(selectedBooksPath: ArrayList<String>): ServerStatus {
var errorMessage: Int? = null
ServerUtils.port = DEFAULT_PORT ServerUtils.port = DEFAULT_PORT
kiwixServer = kiwixServerFactory.createKiwixServer(selectedBooksPath).also { kiwixServer = kiwixServerFactory.createKiwixServer(selectedBooksPath).also {
updateServerState(it.startServer(ServerUtils.port)) updateServerState(it.startServer(ServerUtils.port))
@ -73,7 +89,6 @@ class WebServerHelper @Inject constructor(
} }
} }
} }
}
return ServerStatus(isServerStarted, errorMessage) return ServerStatus(isServerStarted, errorMessage)
} }

View File

@ -94,6 +94,7 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View {
private lateinit var serviceConnection: ServiceConnection private lateinit var serviceConnection: ServiceConnection
private var dialog: Dialog? = null private var dialog: Dialog? = null
private var activityZimHostBinding: ActivityZimHostBinding? = null private var activityZimHostBinding: ActivityZimHostBinding? = null
private var isHotspotServiceRunning = false
override val fragmentTitle: String? by lazy { override val fragmentTitle: String? by lazy {
getString(R.string.menu_wifi_hotspot) getString(R.string.menu_wifi_hotspot)
} }
@ -304,7 +305,11 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View {
} }
private fun stopServer() { private fun stopServer() {
requireActivity().startService(createHotspotIntent(ACTION_STOP_SERVER)) requireActivity().startService(
createHotspotIntent(ACTION_STOP_SERVER)
).also {
isHotspotServiceRunning = false
}
} }
private fun select(bookOnDisk: BooksOnDiskListItem.BookOnDisk) { private fun select(bookOnDisk: BooksOnDiskListItem.BookOnDisk) {
@ -316,6 +321,9 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View {
} }
booksAdapter.items = booksList booksAdapter.items = booksList
saveHostedBooks(booksList) saveHostedBooks(booksList)
if (ServerUtils.isServerStarted) {
startWifiHotspot(true)
}
} }
override fun onStart() { override fun onStart() {
@ -338,6 +346,9 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View {
private fun unbindService() { private fun unbindService() {
hotspotService?.let { hotspotService?.let {
requireActivity().unbindService(serviceConnection) requireActivity().unbindService(serviceConnection)
if (!isHotspotServiceRunning) {
unRegisterHotspotService()
}
} }
} }
@ -370,7 +381,7 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View {
activityZimHostBinding?.startServerButton?.setBackgroundColor( activityZimHostBinding?.startServerButton?.setBackgroundColor(
ContextCompat.getColor(requireActivity(), R.color.stopServerRed) ContextCompat.getColor(requireActivity(), R.color.stopServerRed)
) )
bookDelegate.selectionMode = SelectionMode.NORMAL bookDelegate.selectionMode = SelectionMode.MULTI
booksAdapter.notifyDataSetChanged() booksAdapter.notifyDataSetChanged()
} }
@ -403,11 +414,16 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
activityZimHostBinding?.recyclerViewZimHost?.adapter = null activityZimHostBinding?.recyclerViewZimHost?.adapter = null
hotspotService?.registerCallBack(null) unRegisterHotspotService()
presenter.detachView() presenter.detachView()
activityZimHostBinding = null activityZimHostBinding = null
} }
private fun unRegisterHotspotService() {
hotspotService?.registerCallBack(null)
hotspotService = null
}
// Advice user to turn on hotspot manually for API<26 // Advice user to turn on hotspot manually for API<26
private fun startHotspotManuallyDialog() { private fun startHotspotManuallyDialog() {
@ -487,13 +503,19 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View {
} }
} }
override fun onIpAddressValid() { private fun startWifiHotspot(restartServer: Boolean) {
dialog?.dismiss()
requireActivity().startService( requireActivity().startService(
createHotspotIntent(ACTION_START_SERVER).putStringArrayListExtra( createHotspotIntent(ACTION_START_SERVER).putStringArrayListExtra(
SELECTED_ZIM_PATHS_KEY, selectedBooksPath SELECTED_ZIM_PATHS_KEY, selectedBooksPath
) ).putExtra(RESTART_SERVER, restartServer)
) ).also {
isHotspotServiceRunning = true
}
}
override fun onIpAddressValid() {
dialog?.dismiss()
startWifiHotspot(false)
} }
override fun onIpAddressInvalid() { override fun onIpAddressInvalid() {
@ -503,6 +525,7 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View {
companion object { companion object {
const val SELECTED_ZIM_PATHS_KEY = "selected_zim_paths" const val SELECTED_ZIM_PATHS_KEY = "selected_zim_paths"
const val RESTART_SERVER = "restart_server"
const val PERMISSION_REQUEST_CODE_COARSE_LOCATION = 10 const val PERMISSION_REQUEST_CODE_COARSE_LOCATION = 10
} }
} }

View File

@ -29,6 +29,7 @@ import org.kiwix.kiwixmobile.core.utils.ServerUtils.getSocketAddress
import org.kiwix.kiwixmobile.core.webserver.WebServerHelper import org.kiwix.kiwixmobile.core.webserver.WebServerHelper
import org.kiwix.kiwixmobile.core.webserver.ZimHostCallbacks import org.kiwix.kiwixmobile.core.webserver.ZimHostCallbacks
import org.kiwix.kiwixmobile.core.webserver.ZimHostFragment import org.kiwix.kiwixmobile.core.webserver.ZimHostFragment
import org.kiwix.kiwixmobile.core.webserver.ZimHostFragment.Companion.RESTART_SERVER
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import javax.inject.Inject import javax.inject.Inject
@ -68,22 +69,27 @@ class HotspotService :
super.onDestroy() super.onDestroy()
} }
@Suppress("NestedBlockDepth")
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
when (intent.action) { when (intent.action) {
ACTION_START_SERVER -> ACTION_START_SERVER -> {
val restartServer = intent.getBooleanExtra(RESTART_SERVER, false)
intent.getStringArrayListExtra(ZimHostFragment.SELECTED_ZIM_PATHS_KEY)?.let { intent.getStringArrayListExtra(ZimHostFragment.SELECTED_ZIM_PATHS_KEY)?.let {
val serverStatus = webServerHelper?.startServerHelper(it) val serverStatus = webServerHelper?.startServerHelper(it, restartServer)
if (serverStatus?.isServerStarted == true) { if (serverStatus?.isServerStarted == true) {
zimHostCallbacks?.onServerStarted(getSocketAddress()) zimHostCallbacks?.onServerStarted(getSocketAddress())
startForegroundNotificationHelper() startForegroundNotificationHelper()
if (!restartServer) {
Toast.makeText( Toast.makeText(
this, R.string.server_started_successfully_toast_message, this, R.string.server_started_successfully_toast_message,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
}
} else { } else {
onServerFailedToStart(serverStatus?.errorMessage) onServerFailedToStart(serverStatus?.errorMessage)
} }
} ?: kotlin.run { onServerFailedToStart(R.string.no_books_selected_toast_message) } } ?: kotlin.run { onServerFailedToStart(R.string.no_books_selected_toast_message) }
}
ACTION_STOP_SERVER -> { ACTION_STOP_SERVER -> {
Toast.makeText( Toast.makeText(