diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/BaseActivityTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/BaseActivityTest.kt index 010bddc0e..c2f2b67e2 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/BaseActivityTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/BaseActivityTest.kt @@ -20,7 +20,6 @@ package org.kiwix.kiwixmobile import android.Manifest.permission import android.content.Context -import android.os.Build import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation @@ -35,18 +34,10 @@ import org.kiwix.kiwixmobile.main.KiwixMainActivity abstract class BaseActivityTest { open lateinit var activityScenario: ActivityScenario - private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { - arrayOf( - permission.READ_EXTERNAL_STORAGE, - permission.WRITE_EXTERNAL_STORAGE, - permission.SYSTEM_ALERT_WINDOW - ) - } else { - arrayOf( - permission.READ_EXTERNAL_STORAGE, - permission.WRITE_EXTERNAL_STORAGE - ) - } + private val permissions = arrayOf( + permission.READ_EXTERNAL_STORAGE, + permission.WRITE_EXTERNAL_STORAGE + ) @get:Rule var permissionRules: GrantPermissionRule = diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/NetworkTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/NetworkTest.kt index 908e9e9e1..4be412547 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/NetworkTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/NetworkTest.kt @@ -18,16 +18,15 @@ package org.kiwix.kiwixmobile import android.Manifest -import android.os.Build import android.util.Log import androidx.test.core.app.ActivityScenario -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.espresso.Espresso import androidx.test.espresso.IdlingPolicies import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import com.adevinta.android.barista.interaction.BaristaDialogInteractions @@ -58,18 +57,10 @@ class NetworkTest { // @Inject // MockWebServer mockWebServer - private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { - 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.WRITE_EXTERNAL_STORAGE - ) - } + private val permissions = arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) @Rule @JvmField diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageFragmentTest.kt index d0e1a99f5..e49692736 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageFragmentTest.kt @@ -19,7 +19,6 @@ package org.kiwix.kiwixmobile.language import android.Manifest import android.app.Instrumentation -import android.os.Build import androidx.core.content.edit import androidx.preference.PreferenceManager import androidx.test.ext.junit.rules.ActivityScenarioRule @@ -50,18 +49,10 @@ class LanguageFragmentTest { @get:Rule var activityScenarioRule = ActivityScenarioRule(KiwixMainActivity::class.java) - private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { - 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.WRITE_EXTERNAL_STORAGE - ) - } + private val permissions = arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) @Rule @JvmField diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/nav/destination/library/LibraryRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/nav/destination/library/LibraryRobot.kt index c1691b8ee..1a4667816 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/nav/destination/library/LibraryRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/nav/destination/library/LibraryRobot.kt @@ -19,20 +19,25 @@ package org.kiwix.kiwixmobile.nav.destination.library import android.util.Log +import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.longClick 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.withText import applyWithViewHierarchyPrinting import com.adevinta.android.barista.interaction.BaristaSleepInteractions import org.kiwix.kiwixmobile.BaseRobot -import org.kiwix.kiwixmobile.Findable.Text import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferRobot import org.kiwix.kiwixmobile.localFileTransfer.localFileTransfer import org.kiwix.kiwixmobile.testutils.TestUtils +import org.kiwix.kiwixmobile.utils.RecyclerViewItemCount fun library(func: LibraryRobot.() -> Unit) = LibraryRobot().applyWithViewHierarchyPrinting(func) @@ -60,7 +65,17 @@ class LibraryRobot : BaseRobot() { fun deleteZimIfExists() { 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(recyclerViewItemsCount - 1)) + + for (position in 0 until recyclerViewItemsCount) { + // Long-click the item to select it + onView(withId(recyclerViewId)) + .perform(actionOnItemAtPosition(position, longClick())) + } clickOnFileDeleteIcon() assertDeleteDialogDisplayed() clickOnDeleteZimFile() @@ -75,6 +90,7 @@ class LibraryRobot : BaseRobot() { } private fun clickOnFileDeleteIcon() { + pauseForBetterTestPerformance() clickOn(ViewId(R.id.zim_file_delete_item)) } @@ -84,10 +100,6 @@ class LibraryRobot : BaseRobot() { .check(ViewAssertions.matches(isDisplayed())) } - private fun longClickOnZimFile() { - longClickOn(Text(zimFileTitle)) - } - private fun clickOnDeleteZimFile() { pauseForBetterTestPerformance() onView(withText("DELETE")).perform(click()) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt index 98089dcea..0fa0e8144 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt @@ -18,7 +18,6 @@ package org.kiwix.kiwixmobile.settings import android.Manifest -import android.os.Build import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.internal.runner.junit4.statement.UiThreadStatement import androidx.test.platform.app.InstrumentationRegistry @@ -46,18 +45,10 @@ class KiwixSettingsFragmentTest { @get:Rule var activityScenarioRule = ActivityScenarioRule(KiwixMainActivity::class.java) - private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { - 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.WRITE_EXTERNAL_STORAGE - ) - } + private val permissions = arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) @Rule @JvmField diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/splash/KiwixSplashActivityTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/splash/KiwixSplashActivityTest.kt index b51cfa5cd..e2c0bca15 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/splash/KiwixSplashActivityTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/splash/KiwixSplashActivityTest.kt @@ -19,7 +19,6 @@ package org.kiwix.kiwixmobile.splash import android.Manifest import android.content.Context -import android.os.Build import androidx.preference.PreferenceManager import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso @@ -59,18 +58,10 @@ class KiwixSplashActivityTest { private val activityScenario: ActivityScenario = ActivityScenario.launch(KiwixMainActivity::class.java) - private val permissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { - 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.WRITE_EXTERNAL_STORAGE - ) - } + private val permissions = arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) @Rule @JvmField diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/RecyclerViewItemCount.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/RecyclerViewItemCount.kt new file mode 100644 index 000000000..8e7cddf30 --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/RecyclerViewItemCount.kt @@ -0,0 +1,41 @@ +/* + * Kiwix Android + * Copyright (c) 2023 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.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 + } +} diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/RecyclerViewSelectedCheckBoxCountAssertion.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/RecyclerViewSelectedCheckBoxCountAssertion.kt new file mode 100644 index 000000000..631c04fdd --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/RecyclerViewSelectedCheckBoxCountAssertion.kt @@ -0,0 +1,75 @@ +/* + * Kiwix Android + * Copyright (c) 2023 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.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 = allOf( + isAssignableFrom(RecyclerView::class.java), + isDisplayed(), + withId(recyclerViewId) + ) + + // Use a custom ViewMatcher to find checkboxes that are checked + val checkBoxMatcher: Matcher = object : TypeSafeMatcher() { + 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(checkBoxId) + } + .filter { it != null && checkBoxMatcher.matches(it) } + .forEach { _ -> checkedCount++ } + } + + return checkedCount + } +} diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt new file mode 100644 index 000000000..a359a4431 --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt @@ -0,0 +1,175 @@ +/* + * Kiwix Android + * Copyright (c) 2023 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.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 + + 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 + } + } +} diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostRobot.kt index 26db0ca50..19cc01724 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostRobot.kt @@ -18,10 +18,27 @@ 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 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.Findable.StringId.TextId +import org.kiwix.kiwixmobile.Findable.Text +import org.kiwix.kiwixmobile.Findable.ViewId 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) @@ -30,4 +47,107 @@ class ZimHostRobot : BaseRobot() { fun assertMenuWifiHotspotDiplayed() { 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()) + } } diff --git a/app/src/androidTest/resources/small.zim b/app/src/androidTest/resources/small.zim new file mode 100644 index 000000000..cb7072da6 Binary files /dev/null and b/app/src/androidTest/resources/small.zim differ diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt index bb4d7774e..a579544d0 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt @@ -333,6 +333,7 @@ abstract class CoreReaderFragment : private var readAloudService: ReadAloudService? = null private var navigationHistoryList: MutableList = ArrayList() private var isReadSelection = false + private var isReadAloudServiceRunning = false private var storagePermissionForNotesLauncher: ActivityResultLauncher? = registerForActivityResult( @@ -1047,8 +1048,7 @@ abstract class CoreReaderFragment : } catch (ignore: IllegalArgumentException) { // to handle if service is already unbounded } - readAloudService?.registerCallBack(null) - readAloudService = null + unRegisterReadAloudService() storagePermissionForNotesLauncher?.unregister() storagePermissionForNotesLauncher = null } @@ -2074,9 +2074,17 @@ abstract class CoreReaderFragment : private fun unbindService() { readAloudService?.let { requireActivity().unbindService(serviceConnection) + if (!isReadAloudServiceRunning) { + unRegisterReadAloudService() + } } } + private fun unRegisterReadAloudService() { + readAloudService?.registerCallBack(null) + readAloudService = null + } + private fun createReadAloudIntent(action: String, isPauseTTS: Boolean): Intent = Intent(requireActivity(), ReadAloudService::class.java).apply { setAction(action) @@ -2086,7 +2094,11 @@ abstract class CoreReaderFragment : } 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( diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt index 06d239224..d2025385c 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt @@ -60,8 +60,11 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) { val prefIsFirstRun: Boolean get() = sharedPreferences.getBoolean(PREF_IS_FIRST_RUN, true) - val prefIsTest: Boolean + var prefIsTest: Boolean get() = sharedPreferences.getBoolean(PREF_IS_TEST, false) + set(prefIsTest) { + sharedPreferences.edit { putBoolean(PREF_IS_TEST, prefIsTest) } + } val prefShowShowCaseToUser: Boolean get() = sharedPreferences.getBoolean(PREF_SHOW_SHOWCASE, true) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/WebServerHelper.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/WebServerHelper.kt index 34086f196..e399e2bea 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/WebServerHelper.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/WebServerHelper.kt @@ -45,12 +45,15 @@ class WebServerHelper @Inject constructor( private var isServerStarted = false private var validIpAddressDisposable: Disposable? = null - fun startServerHelper(selectedBooksPath: ArrayList): ServerStatus { + fun startServerHelper( + selectedBooksPath: ArrayList, + restartServer: Boolean + ): ServerStatus? { val ip = getIpAddress() return if (ip.isNullOrEmpty()) { ServerStatus(false, R.string.error_ip_address_not_found) } else { - startAndroidWebServer(selectedBooksPath) + startAndroidWebServer(selectedBooksPath, restartServer) } } @@ -61,16 +64,28 @@ class WebServerHelper @Inject constructor( } } - private fun startAndroidWebServer(selectedBooksPath: ArrayList): ServerStatus { - var errorMessage: Int? = null + private fun startAndroidWebServer( + selectedBooksPath: ArrayList, + restartServer: Boolean + ): ServerStatus? { + var serverStatus: ServerStatus? = null if (!isServerStarted) { - ServerUtils.port = DEFAULT_PORT - kiwixServer = kiwixServerFactory.createKiwixServer(selectedBooksPath).also { - updateServerState(it.startServer(ServerUtils.port)) - Log.d(TAG, "Server status$isServerStarted").also { - if (!isServerStarted) { - errorMessage = R.string.error_server_already_running - } + serverStatus = startKiwixServer(selectedBooksPath) + } else if (restartServer) { + kiwixServer?.stopServer() + serverStatus = startKiwixServer(selectedBooksPath) + } + return serverStatus + } + + private fun startKiwixServer(selectedBooksPath: ArrayList): ServerStatus { + var errorMessage: Int? = null + ServerUtils.port = DEFAULT_PORT + kiwixServer = kiwixServerFactory.createKiwixServer(selectedBooksPath).also { + updateServerState(it.startServer(ServerUtils.port)) + Log.d(TAG, "Server status$isServerStarted").also { + if (!isServerStarted) { + errorMessage = R.string.error_server_already_running } } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/ZimHostFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/ZimHostFragment.kt index 8a48b889e..d9f0052cc 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/ZimHostFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/ZimHostFragment.kt @@ -94,6 +94,7 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View { private lateinit var serviceConnection: ServiceConnection private var dialog: Dialog? = null private var activityZimHostBinding: ActivityZimHostBinding? = null + private var isHotspotServiceRunning = false override val fragmentTitle: String? by lazy { getString(R.string.menu_wifi_hotspot) } @@ -304,7 +305,11 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View { } private fun stopServer() { - requireActivity().startService(createHotspotIntent(ACTION_STOP_SERVER)) + requireActivity().startService( + createHotspotIntent(ACTION_STOP_SERVER) + ).also { + isHotspotServiceRunning = false + } } private fun select(bookOnDisk: BooksOnDiskListItem.BookOnDisk) { @@ -316,6 +321,9 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View { } booksAdapter.items = booksList saveHostedBooks(booksList) + if (ServerUtils.isServerStarted) { + startWifiHotspot(true) + } } override fun onStart() { @@ -338,6 +346,9 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View { private fun unbindService() { hotspotService?.let { requireActivity().unbindService(serviceConnection) + if (!isHotspotServiceRunning) { + unRegisterHotspotService() + } } } @@ -370,7 +381,7 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View { activityZimHostBinding?.startServerButton?.setBackgroundColor( ContextCompat.getColor(requireActivity(), R.color.stopServerRed) ) - bookDelegate.selectionMode = SelectionMode.NORMAL + bookDelegate.selectionMode = SelectionMode.MULTI booksAdapter.notifyDataSetChanged() } @@ -403,11 +414,16 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View { override fun onDestroyView() { super.onDestroyView() activityZimHostBinding?.recyclerViewZimHost?.adapter = null - hotspotService?.registerCallBack(null) + unRegisterHotspotService() presenter.detachView() activityZimHostBinding = null } + private fun unRegisterHotspotService() { + hotspotService?.registerCallBack(null) + hotspotService = null + } + // Advice user to turn on hotspot manually for API<26 private fun startHotspotManuallyDialog() { @@ -487,13 +503,19 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View { } } - override fun onIpAddressValid() { - dialog?.dismiss() + private fun startWifiHotspot(restartServer: Boolean) { requireActivity().startService( createHotspotIntent(ACTION_START_SERVER).putStringArrayListExtra( SELECTED_ZIM_PATHS_KEY, selectedBooksPath - ) - ) + ).putExtra(RESTART_SERVER, restartServer) + ).also { + isHotspotServiceRunning = true + } + } + + override fun onIpAddressValid() { + dialog?.dismiss() + startWifiHotspot(false) } override fun onIpAddressInvalid() { @@ -503,6 +525,7 @@ class ZimHostFragment : BaseFragment(), ZimHostCallbacks, ZimHostContract.View { companion object { const val SELECTED_ZIM_PATHS_KEY = "selected_zim_paths" + const val RESTART_SERVER = "restart_server" const val PERMISSION_REQUEST_CODE_COARSE_LOCATION = 10 } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/wifi_hotspot/HotspotService.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/wifi_hotspot/HotspotService.kt index ac02b404f..ec27f1afc 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/wifi_hotspot/HotspotService.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/webserver/wifi_hotspot/HotspotService.kt @@ -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.ZimHostCallbacks import org.kiwix.kiwixmobile.core.webserver.ZimHostFragment +import org.kiwix.kiwixmobile.core.webserver.ZimHostFragment.Companion.RESTART_SERVER import java.lang.ref.WeakReference import javax.inject.Inject @@ -68,22 +69,27 @@ class HotspotService : super.onDestroy() } + @Suppress("NestedBlockDepth") override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { when (intent.action) { - ACTION_START_SERVER -> + ACTION_START_SERVER -> { + val restartServer = intent.getBooleanExtra(RESTART_SERVER, false) intent.getStringArrayListExtra(ZimHostFragment.SELECTED_ZIM_PATHS_KEY)?.let { - val serverStatus = webServerHelper?.startServerHelper(it) + val serverStatus = webServerHelper?.startServerHelper(it, restartServer) if (serverStatus?.isServerStarted == true) { zimHostCallbacks?.onServerStarted(getSocketAddress()) startForegroundNotificationHelper() - Toast.makeText( - this, R.string.server_started_successfully_toast_message, - Toast.LENGTH_SHORT - ).show() + if (!restartServer) { + Toast.makeText( + this, R.string.server_started_successfully_toast_message, + Toast.LENGTH_SHORT + ).show() + } } else { onServerFailedToStart(serverStatus?.errorMessage) } } ?: kotlin.run { onServerFailedToStart(R.string.no_books_selected_toast_message) } + } ACTION_STOP_SERVER -> { Toast.makeText(