diff --git a/app/build.gradle b/app/build.gradle
index a084b63bf0a6f982e06fa57294caacc1b88ece1e..c7e27d5a8dd77c6e3e7d20b69c5c451da4238d05 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,6 +10,7 @@ plugins {
id 'kotlin-allopen'
id 'kotlin-parcelize'
id 'jacoco'
+ alias libs.plugins.compose.compiler
}
jacoco {
@@ -73,7 +74,7 @@ tasks.withType(Test).configureEach {
}
android {
- compileSdk = 35
+ compileSdk = 36
defaultConfig {
applicationId = "foundation.e.apps"
@@ -178,6 +179,7 @@ android {
buildFeatures {
buildConfig = true
viewBinding = true
+ compose = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
@@ -343,6 +345,26 @@ dependencies {
// JSoup
implementation(libs.jsoup)
+
+ // Compose
+ def composeBom = platform(libs.compose.bom)
+ implementation composeBom
+ androidTestImplementation composeBom
+
+ implementation libs.compose.material3
+ implementation libs.compose.material.icons.extended
+
+ implementation libs.activity.compose
+ implementation libs.lifecycle.viewmodel.compose
+ implementation libs.runtime.livedata
+
+ // Android Studio Preview support for Compose
+ implementation libs.compose.ui.tooling.preview
+ debugImplementation libs.compose.ui.tooling
+
+ // UI Tests for Compose
+ androidTestImplementation libs.compose.ui.test.junit4
+ debugImplementation libs.compose.ui.test.manifest
}
def retrieveKey(String keyName, String defaultValue) {
diff --git a/app/src/main/java/foundation/e/apps/data/DownloadManager.kt b/app/src/main/java/foundation/e/apps/data/DownloadManager.kt
index df1049b47e9ea1fdb4dde3b0d06cbb1050c950ac..b20650f4f9d1273ea3f1c3ce80f438ea3f936efd 100644
--- a/app/src/main/java/foundation/e/apps/data/DownloadManager.kt
+++ b/app/src/main/java/foundation/e/apps/data/DownloadManager.kt
@@ -21,6 +21,7 @@ import android.app.DownloadManager
import android.content.Context
import android.database.Cursor
import android.net.Uri
+import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.OpenForTesting
import foundation.e.apps.R
@@ -78,7 +79,7 @@ class DownloadManager @Inject constructor(
): Long {
var downloadId = -1L
try {
- val request = DownloadManager.Request(Uri.parse(url))
+ val request = DownloadManager.Request(url.toUri())
.setTitle(context.getString(R.string.downloading))
.setDestinationUri(Uri.fromFile(downloadFile))
if (appLoungePreference.isOnlyUnmeteredNetworkEnabled()) {
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 0ac131503fee56ba9aad38a7e1be9ce8d21e0db9..aa5767318247da566778e070b8590ff53b278667 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
@@ -19,6 +19,7 @@
package foundation.e.apps.data.application.data
import android.net.Uri
+import androidx.core.net.toUri
import com.aurora.gplayapi.Constants.Restriction
import com.aurora.gplayapi.data.models.ContentRating
import com.google.gson.annotations.SerializedName
@@ -109,10 +110,10 @@ data class Application(
val Application.shareUri: Uri
get() = when (type) {
- PWA -> Uri.parse(url)
+ PWA -> url.toUri()
NATIVE -> when {
isFDroidApp -> buildFDroidUri(package_name)
- else -> Uri.parse(shareUrl)
+ else -> shareUrl.toUri()
}
}
diff --git a/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt b/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt
index 2899e605b53b0fe705771ded1334e7e2be55dc9e..b7ff07e81d350879c90c5d16d7bbe5beb3659079 100644
--- a/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt
+++ b/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt
@@ -24,6 +24,7 @@ import android.app.NotificationManager
import android.content.Context
import android.net.Uri
import android.os.Environment
+import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
@@ -236,7 +237,7 @@ class AppManagerImpl @Inject constructor(
} else {
context.getString(R.string.additional_file_for, appInstall.name)
}
- val request = DownloadManager.Request(Uri.parse(it))
+ val request = DownloadManager.Request(it.toUri())
.setTitle(requestTitle)
.setDestinationUri(Uri.fromFile(packagePath))
diff --git a/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt b/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt
index c1b63a1093cef351455130be3e819ca0600262bc..649318dcc7f5556e8095aab3c0842031cec74ec4 100644
--- a/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt
+++ b/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt
@@ -7,10 +7,10 @@ import android.content.Intent
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
-import android.net.Uri
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
+import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.OpenForTesting
import foundation.e.apps.data.application.data.Application
@@ -64,7 +64,7 @@ class PwaManager @Inject constructor(
*/
fun getPwaStatus(application: Application): Status {
context.contentResolver.query(
- Uri.parse(PWA_PLAYER),
+ PWA_PLAYER.toUri(),
null,
null,
null,
@@ -106,7 +106,7 @@ class PwaManager @Inject constructor(
*/
fun launchPwa(application: Application) {
val launchIntent = Intent(VIEW_PWA).apply {
- data = Uri.parse(application.url)
+ data = application.url.toUri()
putExtra(PWA_ID, application.pwaPlayerDbId)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS)
@@ -137,7 +137,7 @@ class PwaManager @Inject constructor(
put(ICON, iconByteArray)
}
- context.contentResolver.insert(Uri.parse(PWA_PLAYER), values)?.let {
+ context.contentResolver.insert(PWA_PLAYER.toUri(), values)?.let {
val databaseID = ContentUris.parseId(it)
publishShortcut(appInstall, iconBitmap, databaseID)
}
@@ -170,7 +170,7 @@ class PwaManager @Inject constructor(
val intent = Intent().apply {
action = VIEW_PWA
- data = Uri.parse(appInstall.downloadURLList[0])
+ data = appInstall.downloadURLList[0].toUri()
putExtra(PWA_NAME, appInstall.name)
putExtra(PWA_ID, databaseID)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
diff --git a/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt b/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f7e7dea080bbd61ca3fce5afac51ee260d692608
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.compose.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.colorResource
+import foundation.e.elib.R as eR
+
+@Composable
+fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
+ val colorScheme = if (darkTheme) {
+ darkColorScheme(
+ primary = colorResource(eR.color.e_action_bar_dark),
+ secondary = colorResource(eR.color.e_action_bar_dark),
+ tertiary = colorResource(eR.color.e_accent_dark),
+ background = colorResource(eR.color.e_background_dark),
+ surface = colorResource(eR.color.e_floating_background_dark),
+ onPrimary = colorResource(eR.color.e_primary_text_color_dark),
+ onSecondary = colorResource(eR.color.e_primary_text_color_light),
+ onBackground = colorResource(eR.color.e_primary_text_color_dark),
+ onSurface = colorResource(eR.color.e_primary_text_color_dark),
+ )
+ } else {
+ lightColorScheme(
+ primary = colorResource(eR.color.e_action_bar_light),
+ secondary = colorResource(eR.color.e_action_bar_light),
+ tertiary = colorResource(eR.color.e_accent_light),
+ background = colorResource(eR.color.e_background_light),
+ surface = colorResource(eR.color.e_floating_background_light),
+ onPrimary = colorResource(eR.color.e_primary_text_color_light),
+ onSecondary = colorResource(eR.color.e_primary_text_color_dark),
+ onBackground = colorResource(eR.color.e_primary_text_color_light),
+ onSurface = colorResource(eR.color.e_primary_text_color_light),
+ )
+ }
+
+ MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
+}
diff --git a/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt b/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt
new file mode 100644
index 0000000000000000000000000000000000000000..363f5d87d04218e4a9421a6438d9776fe305143d
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.compose.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.Hyphens
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ )
+)
+
+val titleStyle = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 20.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.02.em,
+ hyphens = Hyphens.Auto,
+)
+
+val summaryStyle = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.01.em,
+ hyphens = Hyphens.Auto,
+)
+
+val categoryStyle = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.01.em,
+)
diff --git a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt
index 20876952480a6c8ac50015e24e9d01caf617df3d..9f5f31e68f4ef91014856d1954df4310756fbfe3 100644
--- a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt
+++ b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt
@@ -19,11 +19,11 @@ package foundation.e.apps.ui.parentFragment
import android.content.Intent
import android.graphics.Paint
-import android.net.Uri
import android.widget.TextView
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
+import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
@@ -235,7 +235,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) {
private fun openTroubleshootingPage() {
val troubleshootUrl = getString(R.string.troubleshootURL, Locale.getDefault().language)
val openUrlIntent = Intent(Intent.ACTION_VIEW)
- openUrlIntent.data = Uri.parse(troubleshootUrl)
+ openUrlIntent.data = troubleshootUrl.toUri()
startActivity(openUrlIntent)
}
diff --git a/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c78d7ff04aa799952eae67b77274f81c91e85741
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.search.v2
+
+import android.os.Bundle
+import android.view.View
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import foundation.e.apps.R
+import foundation.e.apps.ui.compose.theme.AppTheme
+
+class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) {
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val composeView = view.findViewById(R.id.composeView)
+ composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ composeView.setContent {
+ AppTheme { }
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3c178adec62aeb6f3c01d8b487a73044cb447d12
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.search.v2
+
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class SearchViewModelV2 @Inject constructor() : ViewModel()
diff --git a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt
index fb74c7f70e2a00351fc10a50315ccf5c06f10693..394d2824a782de1b3fe973c16e68fbaf37b67801 100644
--- a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt
@@ -21,10 +21,10 @@ package foundation.e.apps.ui.settings
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
-import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
+import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
@@ -140,7 +140,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
val troubleshootUrl = getString(R.string.troubleshootURL, Locale.getDefault().language)
- troubleShootPreference?.intent = Intent(Intent.ACTION_VIEW, Uri.parse(troubleshootUrl))
+ troubleShootPreference?.intent = Intent(Intent.ACTION_VIEW, troubleshootUrl.toUri())
}
/**
diff --git a/app/src/main/res/layout/fragment_search_v2.xml b/app/src/main/res/layout/fragment_search_v2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..eb9fbeab752a411de9fd7f15d8b286e826f7edb5
--- /dev/null
+++ b/app/src/main/res/layout/fragment_search_v2.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/app/src/main/res/navigation/navigation_resource.xml b/app/src/main/res/navigation/navigation_resource.xml
index 7746663611fc07819e5f3b0725cead47dba774e4..9de936fd205258cf7f3a176a347df1c47a7853ec 100644
--- a/app/src/main/res/navigation/navigation_resource.xml
+++ b/app/src/main/res/navigation/navigation_resource.xml
@@ -67,6 +67,11 @@
android:id="@+id/action_searchFragment_to_applicationFragment"
app:destination="@id/applicationFragment" />
+