From 268b411f6e86cc304703deff8e6d71d5a09133ff Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Thu, 21 Aug 2025 17:25:54 +0530 Subject: [PATCH] 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. --- .../kiwixmobile/deeplinks/DeepLinkRobot.kt | 71 +++++++++++++++ .../kiwixmobile/deeplinks/DeepLinksTest.kt | 87 ++++++++++++++++--- app/src/main/AndroidManifest.xml | 8 ++ .../kiwixmobile/main/KiwixMainActivity.kt | 16 ++++ .../core/dao/LibkiwixBookOnDisk.kt | 3 + .../core/main/reader/CoreReaderFragment.kt | 4 +- 6 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinkRobot.kt diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinkRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinkRobot.kt new file mode 100644 index 000000000..7e0cf0575 --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinkRobot.kt @@ -0,0 +1,71 @@ +/* + * Kiwix Android + * Copyright (c) 2025 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.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)) + } + }) + } +} diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinksTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinksTest.kt index 617d42601..3d34ece40 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinksTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinksTest.kt @@ -27,6 +27,8 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.core.content.FileProvider +import androidx.core.net.toUri +import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.espresso.accessibility.AccessibilityChecks import androidx.test.espresso.matcher.ViewMatchers.withContentDescription @@ -44,16 +46,21 @@ import org.junit.Rule import org.junit.Test import org.junit.jupiter.api.fail 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.TestingUtils.COMPOSE_TEST_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.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.testutils.RetryRule import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils.TEST_PAUSE_MS_FOR_DOWNLOAD_TEST import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView +import org.kiwix.kiwixmobile.ui.KiwixDestination import java.io.File import java.io.FileOutputStream import java.io.OutputStream @@ -108,7 +115,9 @@ class DeepLinksTest : BaseActivityTest() { fun fileTypeDeepLinkTest() { loadZimFileInApplicationAndReturnSchemeTypeUri("file")?.let { // Launch the activity to test the deep link - ActivityScenario.launch(createDeepLinkIntent(it)).onActivity {} + ActivityScenario.launch( + createDeepLinkIntent(it, "application/octet-stream") + ).onActivity {} clickOnCopy(composeTestRule) navigationHistory { checkZimFileLoadedSuccessful(composeTestRule) @@ -138,7 +147,9 @@ class DeepLinksTest : BaseActivityTest() { fun contentTypeDeepLinkTest() { loadZimFileInApplicationAndReturnSchemeTypeUri("content")?.let { // Launch the activity to test the deep link - ActivityScenario.launch(createDeepLinkIntent(it)).onActivity {} + ActivityScenario.launch( + createDeepLinkIntent(it, "application/octet-stream") + ).onActivity {} clickOnCopy(composeTestRule) navigationHistory { 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( + 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( + 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? { val loadFileStream = DeepLinksTest::class.java.classLoader.getResourceAsStream("testzim.zim") @@ -180,14 +245,16 @@ class DeepLinksTest : BaseActivityTest() { } } - private fun createDeepLinkIntent(uri: Uri): Intent { - val intent = - Intent(Intent.ACTION_VIEW).apply { - setDataAndType(uri, "application/octet-stream") - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - setPackage(context.packageName) - } - return intent + private fun createDeepLinkIntent( + uri: Uri, + mimeType: String? = null + ): Intent { + return Intent(Intent.ACTION_VIEW).apply { + data = uri + mimeType?.let { setDataAndType(uri, it) } + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + setPackage(context.packageName) + } } @After diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5373b6d18..e651c2add 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -141,6 +141,14 @@ + + + + + + + + { + 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 -> { if (it.scheme != ZIM_HOST_DEEP_LINK_SCHEME) { toast(R.string.cannot_open_file) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookOnDisk.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookOnDisk.kt index 352705d71..3eb93d848 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookOnDisk.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookOnDisk.kt @@ -228,6 +228,9 @@ class LibkiwixBookOnDisk @Inject constructor( 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 * to prevent potential data loss and ensures that the library holds the updated ZIM file data. diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt index fe5c455a2..0c8bb234d 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt @@ -1869,7 +1869,7 @@ abstract class CoreReaderFragment : } private fun startIntentBasedOnAction(intent: Intent?) { - Log.d(TAG_KIWIX, "action" + requireActivity().intent?.action) + Log.d(TAG_KIWIX, "action: ${requireActivity().intent?.action}") when (intent?.action) { Intent.ACTION_PROCESS_TEXT -> { goToSearchWithText(intent) @@ -1903,7 +1903,7 @@ abstract class CoreReaderFragment : // Added condition to handle ZIM files. When opening from storage, the intent may // return null for the type, triggering the search unintentionally. This condition // 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 openSearch(