mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-24 05:04:50 -04:00
Added zim scheme deep link support.
* Refactored code to retrieve the ZIM file from `LibkiwixBookOnDisk` instead of `NewBookDao`, since `NewBookDao` no longer exists and books are now stored in `Libkiwix`. * Added a UI test case for this deep link to verify correct behavior. * Added `testZimHostDeepLink` to specifically test the new compose deep link and help prevent future regressions.
This commit is contained in:
parent
7b4afafa2d
commit
268b411f6e
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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.deeplinks
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertTextEquals
|
||||||
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.test.espresso.web.sugar.Web.onWebView
|
||||||
|
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
|
||||||
|
import androidx.test.espresso.web.webdriver.Locator
|
||||||
|
import applyWithViewHierarchyPrinting
|
||||||
|
import org.kiwix.kiwixmobile.BaseRobot
|
||||||
|
import org.kiwix.kiwixmobile.core.R
|
||||||
|
import org.kiwix.kiwixmobile.core.main.reader.READER_SCREEN_TESTING_TAG
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.components.TOOLBAR_TITLE_TESTING_TAG
|
||||||
|
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
|
||||||
|
|
||||||
|
fun deepLink(func: DeepLinkRobot.() -> Unit) =
|
||||||
|
DeepLinkRobot().applyWithViewHierarchyPrinting(func)
|
||||||
|
|
||||||
|
class DeepLinkRobot : BaseRobot() {
|
||||||
|
fun checkZimFileLoadedSuccessful(composeTestRule: ComposeContentTestRule) {
|
||||||
|
testFlakyView({
|
||||||
|
composeTestRule.apply {
|
||||||
|
waitForIdle()
|
||||||
|
onNodeWithTag(READER_SCREEN_TESTING_TAG).assertExists()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertZimFilePageLoaded(composeTestRule: ComposeContentTestRule) {
|
||||||
|
testFlakyView({
|
||||||
|
composeTestRule.apply {
|
||||||
|
waitForIdle()
|
||||||
|
onWebView()
|
||||||
|
.withElement(
|
||||||
|
findElement(
|
||||||
|
Locator.XPATH,
|
||||||
|
"//*[contains(text(), 'History')]"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkZimHostScreenVisible(composeTestRule: ComposeContentTestRule) {
|
||||||
|
testFlakyView({
|
||||||
|
composeTestRule.apply {
|
||||||
|
waitForIdle()
|
||||||
|
onNodeWithTag(TOOLBAR_TITLE_TESTING_TAG)
|
||||||
|
.assertTextEquals(context.getString(R.string.menu_wifi_hotspot))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,8 @@ import androidx.compose.ui.test.junit4.createComposeRule
|
|||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.espresso.accessibility.AccessibilityChecks
|
import androidx.test.espresso.accessibility.AccessibilityChecks
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||||
@ -44,16 +46,21 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.jupiter.api.fail
|
import org.junit.jupiter.api.fail
|
||||||
import org.kiwix.kiwixmobile.BaseActivityTest
|
import org.kiwix.kiwixmobile.BaseActivityTest
|
||||||
|
import org.kiwix.kiwixmobile.core.main.ZIM_HOST_NAV_DEEP_LINK
|
||||||
|
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.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.core.utils.dialog.ALERT_DIALOG_CONFIRM_BUTTON_TESTING_TAG
|
import org.kiwix.kiwixmobile.core.utils.dialog.ALERT_DIALOG_CONFIRM_BUTTON_TESTING_TAG
|
||||||
import org.kiwix.kiwixmobile.main.KiwixMainActivity
|
import org.kiwix.kiwixmobile.main.KiwixMainActivity
|
||||||
|
import org.kiwix.kiwixmobile.main.OPENING_ZIM_FILE_DELAY
|
||||||
|
import org.kiwix.kiwixmobile.nav.destination.library.library
|
||||||
import org.kiwix.kiwixmobile.page.history.navigationHistory
|
import org.kiwix.kiwixmobile.page.history.navigationHistory
|
||||||
import org.kiwix.kiwixmobile.testutils.RetryRule
|
import org.kiwix.kiwixmobile.testutils.RetryRule
|
||||||
import org.kiwix.kiwixmobile.testutils.TestUtils
|
import org.kiwix.kiwixmobile.testutils.TestUtils
|
||||||
import org.kiwix.kiwixmobile.testutils.TestUtils.TEST_PAUSE_MS_FOR_DOWNLOAD_TEST
|
import org.kiwix.kiwixmobile.testutils.TestUtils.TEST_PAUSE_MS_FOR_DOWNLOAD_TEST
|
||||||
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
|
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
|
||||||
|
import org.kiwix.kiwixmobile.ui.KiwixDestination
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@ -108,7 +115,9 @@ class DeepLinksTest : BaseActivityTest() {
|
|||||||
fun fileTypeDeepLinkTest() {
|
fun fileTypeDeepLinkTest() {
|
||||||
loadZimFileInApplicationAndReturnSchemeTypeUri("file")?.let {
|
loadZimFileInApplicationAndReturnSchemeTypeUri("file")?.let {
|
||||||
// Launch the activity to test the deep link
|
// Launch the activity to test the deep link
|
||||||
ActivityScenario.launch<KiwixMainActivity>(createDeepLinkIntent(it)).onActivity {}
|
ActivityScenario.launch<KiwixMainActivity>(
|
||||||
|
createDeepLinkIntent(it, "application/octet-stream")
|
||||||
|
).onActivity {}
|
||||||
clickOnCopy(composeTestRule)
|
clickOnCopy(composeTestRule)
|
||||||
navigationHistory {
|
navigationHistory {
|
||||||
checkZimFileLoadedSuccessful(composeTestRule)
|
checkZimFileLoadedSuccessful(composeTestRule)
|
||||||
@ -138,7 +147,9 @@ class DeepLinksTest : BaseActivityTest() {
|
|||||||
fun contentTypeDeepLinkTest() {
|
fun contentTypeDeepLinkTest() {
|
||||||
loadZimFileInApplicationAndReturnSchemeTypeUri("content")?.let {
|
loadZimFileInApplicationAndReturnSchemeTypeUri("content")?.let {
|
||||||
// Launch the activity to test the deep link
|
// Launch the activity to test the deep link
|
||||||
ActivityScenario.launch<KiwixMainActivity>(createDeepLinkIntent(it)).onActivity {}
|
ActivityScenario.launch<KiwixMainActivity>(
|
||||||
|
createDeepLinkIntent(it, "application/octet-stream")
|
||||||
|
).onActivity {}
|
||||||
clickOnCopy(composeTestRule)
|
clickOnCopy(composeTestRule)
|
||||||
navigationHistory {
|
navigationHistory {
|
||||||
checkZimFileLoadedSuccessful(composeTestRule)
|
checkZimFileLoadedSuccessful(composeTestRule)
|
||||||
@ -151,6 +162,60 @@ class DeepLinksTest : BaseActivityTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun zimUrlTypeDeepLinkTest() {
|
||||||
|
activityScenario =
|
||||||
|
ActivityScenario.launch(KiwixMainActivity::class.java).apply {
|
||||||
|
moveToState(Lifecycle.State.RESUMED)
|
||||||
|
onActivity {
|
||||||
|
handleLocaleChange(
|
||||||
|
it,
|
||||||
|
"en",
|
||||||
|
SharedPreferenceUtil(context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activityScenario.onActivity {
|
||||||
|
it.navigate(KiwixDestination.Library.route)
|
||||||
|
}
|
||||||
|
library {
|
||||||
|
refreshList(composeTestRule)
|
||||||
|
waitUntilZimFilesRefreshing(composeTestRule)
|
||||||
|
deleteZimIfExists(composeTestRule)
|
||||||
|
}
|
||||||
|
loadZimFileInApplicationAndReturnSchemeTypeUri("file")
|
||||||
|
library {
|
||||||
|
refreshList(composeTestRule)
|
||||||
|
waitUntilZimFilesRefreshing(composeTestRule)
|
||||||
|
}
|
||||||
|
// it tests the zim deep link e.g. (zim://60094d1e-1c9a-a60b-2011-4fb02f8db6c3/A/Android_(operating_system).html)
|
||||||
|
ActivityScenario.launch<KiwixMainActivity>(
|
||||||
|
createDeepLinkIntent("zim://60094d1e-1c9a-a60b-2011-4fb02f8db6c3/A/Android_(operating_system).html".toUri())
|
||||||
|
).onActivity {}
|
||||||
|
// for a bit to properly handle the deep link.
|
||||||
|
composeTestRule.mainClock.advanceTimeBy(OPENING_ZIM_FILE_DELAY + 500)
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
deepLink {
|
||||||
|
checkZimFileLoadedSuccessful(composeTestRule)
|
||||||
|
assertZimFilePageLoaded(composeTestRule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testZimHostDeepLink() {
|
||||||
|
// For testing the deep link triggers when user click on notification of the hotspot.
|
||||||
|
// it should open the WIFI-Hotspot screen.
|
||||||
|
ActivityScenario.launch<KiwixMainActivity>(
|
||||||
|
createDeepLinkIntent(ZIM_HOST_NAV_DEEP_LINK.toUri())
|
||||||
|
).onActivity {}
|
||||||
|
// for a bit to properly handle the deep link.
|
||||||
|
composeTestRule.mainClock.advanceTimeBy(OPENING_ZIM_FILE_DELAY + 500)
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
deepLink {
|
||||||
|
checkZimHostScreenVisible(composeTestRule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadZimFileInApplicationAndReturnSchemeTypeUri(schemeType: String): Uri? {
|
private fun loadZimFileInApplicationAndReturnSchemeTypeUri(schemeType: String): Uri? {
|
||||||
val loadFileStream =
|
val loadFileStream =
|
||||||
DeepLinksTest::class.java.classLoader.getResourceAsStream("testzim.zim")
|
DeepLinksTest::class.java.classLoader.getResourceAsStream("testzim.zim")
|
||||||
@ -180,14 +245,16 @@ class DeepLinksTest : BaseActivityTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDeepLinkIntent(uri: Uri): Intent {
|
private fun createDeepLinkIntent(
|
||||||
val intent =
|
uri: Uri,
|
||||||
Intent(Intent.ACTION_VIEW).apply {
|
mimeType: String? = null
|
||||||
setDataAndType(uri, "application/octet-stream")
|
): Intent {
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
return Intent(Intent.ACTION_VIEW).apply {
|
||||||
setPackage(context.packageName)
|
data = uri
|
||||||
}
|
mimeType?.let { setDataAndType(uri, it) }
|
||||||
return intent
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
setPackage(context.packageName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -141,6 +141,14 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="kiwix" android:host="zimhost" />
|
<data android:scheme="kiwix" android:host="zimhost" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="zim" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
|
@ -83,6 +83,7 @@ import org.kiwix.kiwixmobile.core.main.PAGE_URL_KEY
|
|||||||
import org.kiwix.kiwixmobile.core.main.SHOULD_OPEN_IN_NEW_TAB
|
import org.kiwix.kiwixmobile.core.main.SHOULD_OPEN_IN_NEW_TAB
|
||||||
import org.kiwix.kiwixmobile.core.main.ZIM_FILE_URI_KEY
|
import org.kiwix.kiwixmobile.core.main.ZIM_FILE_URI_KEY
|
||||||
import org.kiwix.kiwixmobile.core.main.ZIM_HOST_DEEP_LINK_SCHEME
|
import org.kiwix.kiwixmobile.core.main.ZIM_HOST_DEEP_LINK_SCHEME
|
||||||
|
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_PREFIX
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
|
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
|
||||||
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
|
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
|
||||||
import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost
|
import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost
|
||||||
@ -309,6 +310,21 @@ class KiwixMainActivity : CoreMainActivity() {
|
|||||||
}, OPENING_ZIM_FILE_DELAY)
|
}, OPENING_ZIM_FILE_DELAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"zim" -> {
|
||||||
|
val zimId = it.host
|
||||||
|
val page = it.encodedPath?.removePrefix("/")
|
||||||
|
if (zimId.isNullOrEmpty() || page.isNullOrEmpty()) {
|
||||||
|
return toast(R.string.cannot_open_file)
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
delay(OPENING_ZIM_FILE_DELAY)
|
||||||
|
val book = libkiwixBookOnDisk.bookById(zimId)
|
||||||
|
?: return@launch toast(R.string.cannot_open_file)
|
||||||
|
openPage("$CONTENT_PREFIX$page", book.zimReaderSource)
|
||||||
|
clearIntentDataAndAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (it.scheme != ZIM_HOST_DEEP_LINK_SCHEME) {
|
if (it.scheme != ZIM_HOST_DEEP_LINK_SCHEME) {
|
||||||
toast(R.string.cannot_open_file)
|
toast(R.string.cannot_open_file)
|
||||||
|
@ -228,6 +228,9 @@ class LibkiwixBookOnDisk @Inject constructor(
|
|||||||
it.zimReaderSource.toDatabase().endsWith(downloadTitle, true)
|
it.zimReaderSource.toDatabase().endsWith(downloadTitle, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun bookById(bookId: String) =
|
||||||
|
getBooks().firstOrNull { it.book.id == bookId }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously writes the library data to their respective file in a background thread
|
* Asynchronously writes the library data to their respective file in a background thread
|
||||||
* to prevent potential data loss and ensures that the library holds the updated ZIM file data.
|
* to prevent potential data loss and ensures that the library holds the updated ZIM file data.
|
||||||
|
@ -1869,7 +1869,7 @@ abstract class CoreReaderFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startIntentBasedOnAction(intent: Intent?) {
|
private fun startIntentBasedOnAction(intent: Intent?) {
|
||||||
Log.d(TAG_KIWIX, "action" + requireActivity().intent?.action)
|
Log.d(TAG_KIWIX, "action: ${requireActivity().intent?.action}")
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
Intent.ACTION_PROCESS_TEXT -> {
|
Intent.ACTION_PROCESS_TEXT -> {
|
||||||
goToSearchWithText(intent)
|
goToSearchWithText(intent)
|
||||||
@ -1903,7 +1903,7 @@ abstract class CoreReaderFragment :
|
|||||||
// Added condition to handle ZIM files. When opening from storage, the intent may
|
// Added condition to handle ZIM files. When opening from storage, the intent may
|
||||||
// return null for the type, triggering the search unintentionally. This condition
|
// return null for the type, triggering the search unintentionally. This condition
|
||||||
// prevents such occurrences.
|
// prevents such occurrences.
|
||||||
intent.scheme !in listOf("file", "content", ZIM_HOST_DEEP_LINK_SCHEME)
|
intent.scheme !in listOf("file", "content", "zim", ZIM_HOST_DEEP_LINK_SCHEME)
|
||||||
) {
|
) {
|
||||||
val searchString = if (intent.data == null) "" else intent.data?.lastPathSegment
|
val searchString = if (intent.data == null) "" else intent.data?.lastPathSegment
|
||||||
openSearch(
|
openSearch(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user