diff --git a/app/build.gradle b/app/build.gradle
index 92932253bf68866badac74bc5c87ce933b614a98..c3595339e226293341bbc20b59c7429b0acc485b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -116,13 +116,17 @@ android {
versionCode = versionMajor * 1000000 + versionMinor * 1000 + versionPatch
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
+ def fdroidHost = "f-droid.org"
+
buildConfigField "String", "BUILD_ID", "\"${getGitHash() + "." + getDate()}\""
buildConfigField("String", "SENTRY_DSN", "\"${getSentryDsn()}\"")
buildConfigField "String", "USER_AGENT", "\"${retrieveKey("user_agent", "Dalvik/2.1.0 (Linux; U; Android %s)")}\""
+ buildConfigField "String", "FDROID_HOST", "\"${fdroidHost}\""
+ buildConfigField "String", "FDROID_REPO_BASE_URL", "\"https://${fdroidHost}/repo/\""
def parentalControlPkgName = "foundation.e.parentalcontrol"
- manifestPlaceholders = [parentalControlPkgName: parentalControlPkgName]
+ manifestPlaceholders = [parentalControlPkgName: parentalControlPkgName, fdroidHost: fdroidHost]
buildConfigField "String", "PACKAGE_NAME_PARENTAL_CONTROL", "\"${parentalControlPkgName}\""
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 50db1448da807e0ec8cb2d301927082e4ff5fc1d..fa07e50d9a37623366cdbcc002171524b56589a1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -121,8 +121,11 @@
-
-
+
+
+
+
+
diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt
index d85ba578f995aee8b28142acf4567f8553df4cb2..d10703978a9e75ddda6459dbf64826650d899cd4 100644
--- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt
+++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt
@@ -23,6 +23,7 @@ import androidx.core.net.toUri
import com.aurora.gplayapi.Constants.Restriction
import com.aurora.gplayapi.data.models.ContentRating
import com.google.gson.annotations.SerializedName
+import foundation.e.apps.BuildConfig
import foundation.e.apps.data.cleanapk.CleanApkRetrofit
import foundation.e.apps.data.enums.FilterLevel
import foundation.e.apps.data.enums.Source
@@ -142,7 +143,7 @@ val Application.shareUri: Uri
private fun buildFDroidUri(packageName: String): Uri {
return Uri.Builder()
.scheme("https")
- .authority("f-droid.org")
+ .authority(BuildConfig.FDROID_HOST)
.appendPath("packages")
.appendPath(packageName)
.build()
diff --git a/app/src/main/java/foundation/e/apps/data/di/network/RetrofitApiModule.kt b/app/src/main/java/foundation/e/apps/data/di/network/RetrofitApiModule.kt
index b25d776b6901fb60a0a68a3167bef53d9deb165b..f4f579daf4bf3736ae5faa6cbbc4f6101a8f8f7c 100644
--- a/app/src/main/java/foundation/e/apps/data/di/network/RetrofitApiModule.kt
+++ b/app/src/main/java/foundation/e/apps/data/di/network/RetrofitApiModule.kt
@@ -24,6 +24,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import foundation.e.apps.BuildConfig
import foundation.e.apps.data.cleanapk.CleanApkRetrofit
import foundation.e.apps.data.di.network.NetworkModule.getYamlFactory
import foundation.e.apps.data.ecloud.EcloudApiInterface
@@ -103,7 +104,7 @@ class RetrofitApiModule {
@Provides
fun provideFDroidMonitorApi(okHttpClient: OkHttpClient, moshi: Moshi): FDroidMonitorApi {
return Retrofit.Builder()
- .baseUrl(FDroidMonitorApi.BASE_URL)
+ .baseUrl(BuildConfig.FDROID_REPO_BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
diff --git a/app/src/main/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidMonitorApi.kt b/app/src/main/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidMonitorApi.kt
index dcc01b76444a297709bbafc458fee197f2071953..3496bc718823571277db296b5e6d5433c6da3a42 100644
--- a/app/src/main/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidMonitorApi.kt
+++ b/app/src/main/java/foundation/e/apps/data/parentalcontrol/fdroid/FDroidMonitorApi.kt
@@ -23,10 +23,6 @@ import retrofit2.http.GET
interface FDroidMonitorApi {
- companion object {
- const val BASE_URL = "https://f-droid.org/repo/"
- }
-
@GET("status/update.json")
suspend fun getMonitorData(): Response
}
diff --git a/app/src/main/java/foundation/e/apps/ui/FDroidDeepLinkParser.kt b/app/src/main/java/foundation/e/apps/ui/FDroidDeepLinkParser.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c6f2855361e2f8ac5f89f86b5549cd6933323ed7
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/ui/FDroidDeepLinkParser.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2026 e Foundation
+ *
+ * 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 foundation.e.apps.ui
+
+import android.net.Uri
+import foundation.e.apps.BuildConfig
+
+internal object FDroidDeepLinkParser {
+ private const val PACKAGES_PATH_SEGMENT = "packages"
+ private const val DIRECT_PACKAGE_SEGMENT_COUNT = 2
+ private const val LOCALIZED_PACKAGE_SEGMENT_COUNT = 3
+ private const val LOCALIZED_PACKAGES_SEGMENT_INDEX = 1
+
+ fun getPackageName(uri: Uri?): String? {
+ if (uri == null || !uri.host.equals(BuildConfig.FDROID_HOST, ignoreCase = true)) {
+ return null
+ }
+
+ val pathSegments = uri.pathSegments
+ return when {
+ pathSegments.isDirectPackagePath() || pathSegments.isLocalizedPackagePath() -> pathSegments.last()
+ else -> null
+ }?.takeIf { it.isNotBlank() }
+ }
+
+ private fun List.isDirectPackagePath(): Boolean {
+ return size == DIRECT_PACKAGE_SEGMENT_COUNT && first() == PACKAGES_PATH_SEGMENT
+ }
+
+ private fun List.isLocalizedPackagePath(): Boolean {
+ return size == LOCALIZED_PACKAGE_SEGMENT_COUNT && get(LOCALIZED_PACKAGES_SEGMENT_INDEX) == PACKAGES_PATH_SEGMENT
+ }
+}
diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt
index 92a05c5e15ee6fabf5425475e4553cd6917b2e75..e972acd6d60a374a102360d914d562f90749deb2 100644
--- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt
+++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt
@@ -47,6 +47,7 @@ import foundation.e.apps.BuildConfig
import foundation.e.apps.R
import foundation.e.apps.contract.ParentalControlContract.COLUMN_LOGIN_TYPE
import foundation.e.apps.data.Constants
+import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.event.AppEvent
import foundation.e.apps.data.event.EventBus
import foundation.e.apps.data.install.models.AppInstall
@@ -55,6 +56,7 @@ import foundation.e.apps.data.login.core.StoreType
import foundation.e.apps.data.system.ParentalControlAuthenticator
import foundation.e.apps.databinding.ActivityMainBinding
import foundation.e.apps.domain.model.User
+import foundation.e.apps.ui.application.ApplicationFragmentArgs
import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment
import foundation.e.apps.ui.error.AppUnavailableDialogDirections
import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections
@@ -123,6 +125,10 @@ class MainActivity : AppCompatActivity() {
val (bottomNavigationView, navController) = setupBootomNav()
+ if (savedInstanceState == null) {
+ handleFdroidDeepLink(navController, intent)
+ }
+
setupViewModels()
setupNavigations(navController)
@@ -162,8 +168,33 @@ class MainActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
+ setIntent(intent)
checkGPlayLoginRequest(intent)
- findNavController(R.id.fragment).handleDeepLink(intent)
+ val navController = findNavController(R.id.fragment)
+ if (handleFdroidDeepLink(navController, intent)) {
+ return
+ }
+ navController.handleDeepLink(intent)
+ }
+
+ private fun handleFdroidDeepLink(navController: NavController, intent: Intent?): Boolean {
+ val packageName = FDroidDeepLinkParser.getPackageName(intent?.data) ?: return false
+
+ val args = ApplicationFragmentArgs(
+ id = "",
+ packageName = packageName,
+ source = Source.OPEN_SOURCE,
+ category = "",
+ isGplayReplaced = false,
+ isPurchased = false,
+ ).toBundle()
+
+ val navOptions = NavOptions.Builder()
+ .setLaunchSingleTop(true)
+ .build()
+ navController.navigate(R.id.applicationFragment, args, navOptions)
+
+ return true
}
private fun checkGPlayLoginRequest(intent: Intent?) {
diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt
index 047850460ff04db0deb34c1635e6ff36d4b3598f..f5f65d2aac4b1db08f452dd06833edfe68e93601 100644
--- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt
+++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt
@@ -50,6 +50,7 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textview.MaterialTextView
import dagger.hilt.android.AndroidEntryPoint
+import foundation.e.apps.BuildConfig
import foundation.e.apps.R
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.shareUri
@@ -102,7 +103,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
* Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509
*/
private val isFdroidDeepLink: Boolean by lazy {
- activity?.intent?.data?.host?.equals("f-droid.org") ?: false
+ activity?.intent?.data?.host?.equals(BuildConfig.FDROID_HOST) ?: false
}
private var isDetailsLoaded = false
diff --git a/app/src/main/res/navigation/navigation_resource.xml b/app/src/main/res/navigation/navigation_resource.xml
index 6d074f39f57fe31b69c663b0cefcdf2ba41aaa69..60d9c631e1f4b3a1298c63e7368b97ea054292b7 100644
--- a/app/src/main/res/navigation/navigation_resource.xml
+++ b/app/src/main/res/navigation/navigation_resource.xml
@@ -129,19 +129,10 @@
-
-
-
+
diff --git a/app/src/test/java/foundation/e/apps/data/application/data/ApplicationTest.kt b/app/src/test/java/foundation/e/apps/data/application/data/ApplicationTest.kt
index c47d52642a26d9c9e89db52ee33a70e8bce21e50..d9591298b265ffe432f46abdb6140aea123b1abd 100644
--- a/app/src/test/java/foundation/e/apps/data/application/data/ApplicationTest.kt
+++ b/app/src/test/java/foundation/e/apps/data/application/data/ApplicationTest.kt
@@ -1,6 +1,7 @@
package foundation.e.apps.data.application.data
import com.google.common.truth.Truth.assertThat
+import foundation.e.apps.BuildConfig
import foundation.e.apps.data.cleanapk.CleanApkRetrofit
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Type
@@ -39,7 +40,7 @@ class ApplicationTest {
val uri = app.shareUri
- assertThat(uri.toString()).isEqualTo("https://f-droid.org/packages/org.fdroid.app")
+ assertThat(uri.toString()).isEqualTo("https://${BuildConfig.FDROID_HOST}/packages/org.fdroid.app")
}
@Test
diff --git a/app/src/test/java/foundation/e/apps/ui/FDroidDeepLinkParserTest.kt b/app/src/test/java/foundation/e/apps/ui/FDroidDeepLinkParserTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b706d8611b6099e66e13495daf9b19359c300e9b
--- /dev/null
+++ b/app/src/test/java/foundation/e/apps/ui/FDroidDeepLinkParserTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2026 e Foundation
+ *
+ * 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 foundation.e.apps.ui
+
+import android.net.Uri
+import com.google.common.truth.Truth.assertThat
+import foundation.e.apps.BuildConfig
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class FDroidDeepLinkParserTest {
+
+ @Test
+ fun getPackageName_returnsPackageForNonLocalizedUrl() {
+ val packageName = FDroidDeepLinkParser.getPackageName(
+ Uri.parse("https://${BuildConfig.FDROID_HOST}/packages/org.fdroid.fdroid")
+ )
+
+ assertThat(packageName).isEqualTo("org.fdroid.fdroid")
+ }
+
+ @Test
+ fun getPackageName_returnsPackageForLocalizedUrlWithTrailingSlash() {
+ val packageName = FDroidDeepLinkParser.getPackageName(
+ Uri.parse("https://${BuildConfig.FDROID_HOST}/en/packages/org.fdroid.fdroid/")
+ )
+
+ assertThat(packageName).isEqualTo("org.fdroid.fdroid")
+ }
+
+ @Test
+ fun getPackageName_returnsNullWhenPackageNameIsMissing() {
+ val packageName = FDroidDeepLinkParser.getPackageName(
+ Uri.parse("https://${BuildConfig.FDROID_HOST}/en/packages/")
+ )
+
+ assertThat(packageName).isNull()
+ }
+
+ @Test
+ fun getPackageName_returnsNullWhenPathHasExtraSegments() {
+ val packageName = FDroidDeepLinkParser.getPackageName(
+ Uri.parse("https://${BuildConfig.FDROID_HOST}/en/packages/org.fdroid.fdroid/stats")
+ )
+
+ assertThat(packageName).isNull()
+ }
+
+ @Test
+ fun getPackageName_returnsNullForNonFdroidHost() {
+ val packageName = FDroidDeepLinkParser.getPackageName(
+ Uri.parse("https://example.org/en/packages/org.fdroid.fdroid")
+ )
+
+ assertThat(packageName).isNull()
+ }
+}