From 54e3ad4d8ade52912bd296d867dd3fb705bd5e4a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 27 Oct 2025 18:00:06 -0300 Subject: [PATCH] Support for SOCKS proxy adds a new setting and exposes the proxy in all those places where we do network requests --- .../org/fdroid/download/DownloadModule.kt | 11 +- .../fdroid/download/DownloaderFactoryImpl.kt | 6 +- .../kotlin/org/fdroid/download/ImageModel.kt | 5 +- .../org/fdroid/settings/SettingsConstants.kt | 3 + .../org/fdroid/settings/SettingsManager.kt | 14 ++ next/src/main/kotlin/org/fdroid/ui/Main.kt | 1 + .../org/fdroid/ui/apps/MyAppsViewModel.kt | 5 +- .../org/fdroid/ui/details/AppDetailsHeader.kt | 1 + .../org/fdroid/ui/details/AppDetailsItem.kt | 23 +++- .../fdroid/ui/details/AppDetailsViewModel.kt | 3 + .../org/fdroid/ui/details/DetailsPresenter.kt | 3 + .../org/fdroid/ui/details/RepoChooser.kt | 14 +- .../fdroid/ui/discover/DiscoverViewModel.kt | 19 ++- .../org/fdroid/ui/lists/AppListViewModel.kt | 6 +- .../org/fdroid/ui/repositories/RepoIcon.kt | 5 +- .../ui/repositories/RepositoriesViewModel.kt | 2 +- .../fdroid/ui/repositories/RepositoryItem.kt | 5 +- .../org/fdroid/ui/repositories/add/AddRepo.kt | 3 + .../ui/repositories/add/AddRepoIntro.kt | 4 +- .../repositories/add/AddRepoPreviewScreen.kt | 6 +- .../ui/repositories/add/AddRepoViewModel.kt | 7 +- .../ui/repositories/add/RepoPreviewHeader.kt | 8 +- .../details/RepoDetailsContent.kt | 7 +- .../repositories/details/RepoDetailsHeader.kt | 6 +- .../repositories/details/RepoDetailsInfo.kt | 2 + .../details/RepoDetailsPresenter.kt | 3 + .../details/RepoDetailsViewModel.kt | 3 + .../org/fdroid/ui/settings/PreferenceProxy.kt | 128 ++++++++++++++++++ .../kotlin/org/fdroid/ui/settings/Settings.kt | 12 ++ .../org/fdroid/ui/utils/PreviewUtils.kt | 2 + .../org/fdroid/updates/UpdatesManager.kt | 5 +- next/src/main/res/values/strings-next.xml | 7 + 32 files changed, 285 insertions(+), 44 deletions(-) create mode 100644 next/src/main/kotlin/org/fdroid/ui/settings/PreferenceProxy.kt diff --git a/next/src/main/kotlin/org/fdroid/download/DownloadModule.kt b/next/src/main/kotlin/org/fdroid/download/DownloadModule.kt index 487d2b9c9..aa4493c17 100644 --- a/next/src/main/kotlin/org/fdroid/download/DownloadModule.kt +++ b/next/src/main/kotlin/org/fdroid/download/DownloadModule.kt @@ -5,6 +5,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.fdroid.BuildConfig +import org.fdroid.settings.SettingsManager import javax.inject.Singleton @Module @@ -15,13 +16,13 @@ object DownloadModule { @Provides @Singleton - fun provideHttpManager(): HttpManager { - return HttpManager(userAgent = USER_AGENT) + fun provideHttpManager(settingsManager: SettingsManager): HttpManager { + return HttpManager(userAgent = USER_AGENT, proxyConfig = settingsManager.proxyConfig) } @Provides @Singleton - fun provideDownloaderFactory(httpManager: HttpManager): DownloaderFactory { - return DownloaderFactoryImpl(httpManager) - } + fun provideDownloaderFactory( + downloaderFactoryImpl: DownloaderFactoryImpl, + ): DownloaderFactory = downloaderFactoryImpl } diff --git a/next/src/main/kotlin/org/fdroid/download/DownloaderFactoryImpl.kt b/next/src/main/kotlin/org/fdroid/download/DownloaderFactoryImpl.kt index 911ff342a..56a139eac 100644 --- a/next/src/main/kotlin/org/fdroid/download/DownloaderFactoryImpl.kt +++ b/next/src/main/kotlin/org/fdroid/download/DownloaderFactoryImpl.kt @@ -5,11 +5,15 @@ import android.net.Uri import org.fdroid.IndexFile import org.fdroid.database.Repository import org.fdroid.index.IndexFormatVersion +import org.fdroid.settings.SettingsManager import java.io.File import javax.inject.Inject +import javax.inject.Singleton +@Singleton class DownloaderFactoryImpl @Inject constructor( private val httpManager: HttpManager, + private val settingsManager: SettingsManager, ) : DownloaderFactory() { override fun create( repo: Repository, @@ -31,7 +35,7 @@ class DownloaderFactoryImpl @Inject constructor( val request = DownloadRequest( indexFile = indexFile, mirrors = mirrors, - proxy = null, + proxy = settingsManager.proxyConfig, username = repo.username, password = repo.password, tryFirstMirror = tryFirst, diff --git a/next/src/main/kotlin/org/fdroid/download/ImageModel.kt b/next/src/main/kotlin/org/fdroid/download/ImageModel.kt index 59181ef1a..df433a0ea 100644 --- a/next/src/main/kotlin/org/fdroid/download/ImageModel.kt +++ b/next/src/main/kotlin/org/fdroid/download/ImageModel.kt @@ -2,10 +2,11 @@ package org.fdroid.download import android.net.Uri import androidx.core.net.toUri +import io.ktor.client.engine.ProxyConfig import org.fdroid.IndexFile import org.fdroid.database.Repository -fun IndexFile.getImageModel(repository: Repository?): Any? { +fun IndexFile.getImageModel(repository: Repository?, proxyConfig: ProxyConfig?): Any? { if (repository == null) return null val address = repository.address if (address.startsWith("content://") || address.startsWith("file://")) { @@ -14,7 +15,7 @@ fun IndexFile.getImageModel(repository: Repository?): Any? { return DownloadRequest( indexFile = this, mirrors = repository.getMirrors(), - proxy = null, // TODO proxy support + proxy = proxyConfig, username = repository.username, password = repository.password, ) diff --git a/next/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt b/next/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt index 72ddd7459..64987a885 100644 --- a/next/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt +++ b/next/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt @@ -13,6 +13,9 @@ object SettingsConstants { const val PREF_KEY_AUTO_UPDATES = "autoUpdates" const val PREF_DEFAULT_AUTO_UPDATES = true + const val PREF_KEY_PROXY = "proxy" + const val PREF_DEFAULT_PROXY = "" + const val PREF_KEY_SHOW_INCOMPATIBLE = "incompatibleVersions" const val PREF_DEFAULT_SHOW_INCOMPATIBLE = true diff --git a/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt b/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt index 12c007138..15e5e8f90 100644 --- a/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt +++ b/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt @@ -4,6 +4,8 @@ import android.content.Context import android.content.Context.MODE_PRIVATE import androidx.core.content.edit import dagger.hilt.android.qualifiers.ApplicationContext +import io.ktor.client.engine.ProxyBuilder +import io.ktor.client.engine.ProxyConfig import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map @@ -14,11 +16,13 @@ import org.fdroid.database.AppListSortOrder import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_APP_LIST_SORT_ORDER import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_AUTO_UPDATES import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_LAST_UPDATE_CHECK +import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PROXY import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_SHOW_INCOMPATIBLE import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_THEME import org.fdroid.settings.SettingsConstants.PREF_KEY_APP_LIST_SORT_ORDER import org.fdroid.settings.SettingsConstants.PREF_KEY_AUTO_UPDATES import org.fdroid.settings.SettingsConstants.PREF_KEY_LAST_UPDATE_CHECK +import org.fdroid.settings.SettingsConstants.PREF_KEY_PROXY import org.fdroid.settings.SettingsConstants.PREF_KEY_SHOW_INCOMPATIBLE import org.fdroid.settings.SettingsConstants.PREF_KEY_THEME import org.fdroid.settings.SettingsConstants.getAppListSortOrder @@ -61,6 +65,16 @@ class SettingsManager @Inject constructor( private val _lastRepoUpdateFlow = MutableStateFlow(lastRepoUpdate) val lastRepoUpdateFlow = _lastRepoUpdateFlow.asStateFlow() + val proxyConfig: ProxyConfig? + get() { + val proxyStr = prefs.getString(PREF_KEY_PROXY, PREF_DEFAULT_PROXY) + return if (proxyStr.isNullOrBlank()) null + else { + val (host, port) = proxyStr.split(':') + ProxyBuilder.socks(host, port.toInt()) + } + } + val filterIncompatible: Boolean get() = !prefs.getBoolean(PREF_KEY_SHOW_INCOMPATIBLE, PREF_DEFAULT_SHOW_INCOMPATIBLE) val appListSortOrder: AppListSortOrder diff --git a/next/src/main/kotlin/org/fdroid/ui/Main.kt b/next/src/main/kotlin/org/fdroid/ui/Main.kt index e570a5528..56770d421 100644 --- a/next/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/next/src/main/kotlin/org/fdroid/ui/Main.kt @@ -250,6 +250,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) { } AddRepo( state = viewModel.state.collectAsStateWithLifecycle().value, + proxyConfig = viewModel.proxyConfig, onFetchRepo = viewModel::onFetchRepo, onAddRepo = viewModel::addFetchedRepository, onExistingRepo = { repoId -> diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt index 6cf2f6809..fe3febac7 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt @@ -24,6 +24,7 @@ import org.fdroid.download.getImageModel import org.fdroid.index.RepoManager import org.fdroid.install.AppInstallManager import org.fdroid.install.InstallState +import org.fdroid.settings.SettingsManager import org.fdroid.updates.UpdatesManager import org.fdroid.utils.IoDispatcher import javax.inject.Inject @@ -34,6 +35,7 @@ class MyAppsViewModel @Inject constructor( @param:IoDispatcher private val scope: CoroutineScope, savedStateHandle: SavedStateHandle, private val db: FDroidDatabase, + private val settingsManager: SettingsManager, private val appInstallManager: AppInstallManager, private val updatesManager: UpdatesManager, private val repoManager: RepoManager, @@ -49,6 +51,7 @@ class MyAppsViewModel @Inject constructor( private var installedAppsLiveData = db.getAppDao().getInstalledAppListItems(application.packageManager) private val installedAppsObserver = Observer> { list -> + val proxyConfig = settingsManager.proxyConfig installedApps.value = list.map { app -> InstalledAppItem( packageName = app.packageName, @@ -56,7 +59,7 @@ class MyAppsViewModel @Inject constructor( installedVersionName = app.installedVersionName ?: "???", lastUpdated = app.lastUpdated, iconModel = repoManager.getRepository(app.repoId)?.let { repo -> - app.getIcon(localeList)?.getImageModel(repo) + app.getIcon(localeList)?.getImageModel(repo, proxyConfig) }, ) } diff --git a/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt b/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt index 8198f5c98..3cac3bc80 100644 --- a/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt +++ b/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt @@ -159,6 +159,7 @@ fun AppDetailsHeader( repos = item.repositories, currentRepoId = item.app.repoId, preferredRepoId = item.preferredRepoId, + proxy = item.proxy, onRepoChanged = item.actions.onRepoChanged, onPreferredRepoChanged = item.actions.onPreferredRepoChanged, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), diff --git a/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt b/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt index 0c46b198c..c52aca621 100644 --- a/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt +++ b/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt @@ -5,6 +5,7 @@ import android.os.Build.VERSION.SDK_INT import androidx.activity.result.ActivityResult import androidx.annotation.VisibleForTesting import androidx.core.os.LocaleListCompat +import io.ktor.client.engine.ProxyConfig import org.fdroid.database.App import org.fdroid.database.AppMetadata import org.fdroid.database.AppPrefs @@ -63,6 +64,7 @@ data class AppDetailsItem( */ val noUpdatesBecauseDifferentSigner: Boolean = false, val authorHasMoreThanOneApp: Boolean = false, + val proxy: ProxyConfig?, ) { constructor( repository: Repository, @@ -80,6 +82,7 @@ data class AppDetailsItem( noUpdatesBecauseDifferentSigner: Boolean, authorHasMoreThanOneApp: Boolean, localeList: LocaleListCompat, + proxy: ProxyConfig?, ) : this( app = dbApp.metadata, actions = actions, @@ -89,10 +92,10 @@ data class AppDetailsItem( name = dbApp.name ?: "Unknown App", summary = dbApp.summary, description = getHtmlDescription(dbApp.getDescription(localeList)), - icon = dbApp.getIcon(localeList)?.getImageModel(repository), - featureGraphic = dbApp.getFeatureGraphic(localeList)?.getImageModel(repository), + icon = dbApp.getIcon(localeList)?.getImageModel(repository, proxy), + featureGraphic = dbApp.getFeatureGraphic(localeList)?.getImageModel(repository, proxy), phoneScreenshots = dbApp.getPhoneScreenshots(localeList).mapNotNull { - it.getImageModel(repository) + it.getImageModel(repository, proxy) }, categories = dbApp.metadata.categories?.mapNotNull { categoryId -> val category = repository.getCategories()[categoryId] ?: return@mapNotNull null @@ -108,11 +111,16 @@ data class AppDetailsItem( possibleUpdate = possibleUpdate, appPrefs = appPrefs, whatsNew = installedVersion?.getWhatsNew(localeList), - antiFeatures = installedVersion?.getAntiFeatures(repository, localeList) - ?: suggestedVersion?.getAntiFeatures(repository, localeList) - ?: (versions?.first()?.version as? AppVersion).getAntiFeatures(repository, localeList), + antiFeatures = installedVersion?.getAntiFeatures(repository, localeList, proxy) + ?: suggestedVersion?.getAntiFeatures(repository, localeList, proxy) + ?: (versions?.first()?.version as? AppVersion).getAntiFeatures( + repository = repository, + localeList = localeList, + proxy = proxy, + ), noUpdatesBecauseDifferentSigner = noUpdatesBecauseDifferentSigner, authorHasMoreThanOneApp = authorHasMoreThanOneApp, + proxy = proxy, ) /** @@ -242,12 +250,13 @@ data class AntiFeature( private fun AppVersion?.getAntiFeatures( repository: Repository, localeList: LocaleListCompat, + proxy: ProxyConfig?, ): List? { return this?.antiFeatureKeys?.mapNotNull { key -> val antiFeature = repository.getAntiFeatures()[key] ?: return@mapNotNull null AntiFeature( id = key, - icon = antiFeature.getIcon(localeList)?.getImageModel(repository), + icon = antiFeature.getIcon(localeList)?.getImageModel(repository, proxy), name = antiFeature.getName(localeList) ?: key, reason = getAntiFeatureReason(key, localeList), ) diff --git a/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt index 06c1a7d5b..14fd8b6e7 100644 --- a/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt @@ -33,6 +33,7 @@ import org.fdroid.index.RELEASE_CHANNEL_BETA import org.fdroid.index.RepoManager import org.fdroid.install.AppInstallManager import org.fdroid.install.InstallState +import org.fdroid.settings.SettingsManager import org.fdroid.updates.UpdatesManager import org.fdroid.utils.IoDispatcher import javax.inject.Inject @@ -45,6 +46,7 @@ class AppDetailsViewModel @Inject constructor( private val repoManager: RepoManager, private val updateChecker: UpdateChecker, private val updatesManager: UpdatesManager, + private val settingsManager: SettingsManager, private val appInstallManager: AppInstallManager, ) : AndroidViewModel(app) { private val log = KotlinLogging.logger { } @@ -62,6 +64,7 @@ class AppDetailsViewModel @Inject constructor( viewModel = this, packageInfoFlow = packageInfoFlow, currentRepoIdFlow = currentRepoIdFlow, + settingsManager = settingsManager, ) } diff --git a/next/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/next/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index dcabc72ac..e162d5d2d 100644 --- a/next/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/next/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -17,6 +17,7 @@ import org.fdroid.database.Repository import org.fdroid.index.RepoManager import org.fdroid.install.AppInstallManager import org.fdroid.install.InstallState +import org.fdroid.settings.SettingsManager import org.fdroid.utils.sha256 private const val TAG = "DetailsPresenter" @@ -28,6 +29,7 @@ fun DetailsPresenter( db: FDroidDatabase, repoManager: RepoManager, updateChecker: UpdateChecker, + settingsManager: SettingsManager, appInstallManager: AppInstallManager, viewModel: AppDetailsViewModel, packageInfoFlow: StateFlow, @@ -190,6 +192,7 @@ fun DetailsPresenter( noUpdatesBecauseDifferentSigner = noUpdatesBecauseDifferentSigner, authorHasMoreThanOneApp = authorHasMoreThanOneApp, localeList = locales, + proxy = settingsManager.proxyConfig, ) } diff --git a/next/src/main/kotlin/org/fdroid/ui/details/RepoChooser.kt b/next/src/main/kotlin/org/fdroid/ui/details/RepoChooser.kt index 06ac40f71..b32beb3d5 100644 --- a/next/src/main/kotlin/org/fdroid/ui/details/RepoChooser.kt +++ b/next/src/main/kotlin/org/fdroid/ui/details/RepoChooser.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat +import io.ktor.client.engine.ProxyConfig import org.fdroid.R import org.fdroid.database.Repository import org.fdroid.fdroid.ui.theme.FDroidContent @@ -43,6 +44,7 @@ fun RepoChooser( repos: List, currentRepoId: Long, preferredRepoId: Long, + proxy: ProxyConfig?, onRepoChanged: (Long) -> Unit, onPreferredRepoChanged: (Long) -> Unit, modifier: Modifier = Modifier, @@ -79,7 +81,7 @@ fun RepoChooser( } }, leadingIcon = { - RepoIcon(repo = currentRepo, modifier = Modifier.size(24.dp)) + RepoIcon(repo = currentRepo, proxy = proxy, modifier = Modifier.size(24.dp)) }, trailingIcon = { if (repos.size > 1) Icon( @@ -115,6 +117,7 @@ fun RepoChooser( RepoMenuItem( repo = repo, isPreferred = repo.repoId == preferredRepoId, + proxy = proxy, onClick = { onRepoChanged(repo.repoId) expanded = false @@ -140,6 +143,7 @@ fun RepoChooser( private fun RepoMenuItem( repo: Repository, isPreferred: Boolean, + proxy: ProxyConfig?, modifier: Modifier = Modifier, onClick: () -> Unit ) { @@ -152,7 +156,7 @@ private fun RepoMenuItem( }, modifier = modifier, onClick = onClick, - leadingIcon = { RepoIcon(repo, Modifier.size(24.dp)) } + leadingIcon = { RepoIcon(repo, proxy, Modifier.size(24.dp)) } ) } @@ -172,7 +176,7 @@ private fun getRepoString(repo: Repository, isPreferred: Boolean) = buildAnnotat fun RepoChooserSingleRepoPreview() { val repo1 = Repository(1L, "1", 1L, TWO, "null", 1L, 1, 1L) FDroidContent(pureBlack = true) { - RepoChooser(listOf(repo1), 1L, 1L, {}, {}) + RepoChooser(listOf(repo1), 1L, 1L, null, {}, {}) } } @@ -183,7 +187,7 @@ fun RepoChooserPreview() { val repo2 = Repository(2L, "2", 2L, TWO, "null", 2L, 2, 2L) val repo3 = Repository(3L, "2", 3L, TWO, "null", 3L, 3, 3L) FDroidContent(pureBlack = true) { - RepoChooser(listOf(repo1, repo2, repo3), 1L, 1L, {}, {}) + RepoChooser(listOf(repo1, repo2, repo3), 1L, 1L, null, {}, {}) } } @@ -194,6 +198,6 @@ fun RepoChooserNightPreview() { val repo2 = Repository(2L, "2", 2L, TWO, "null", 2L, 2, 2L) val repo3 = Repository(3L, "2", 3L, TWO, "null", 3L, 3, 3L) FDroidContent(pureBlack = true) { - RepoChooser(listOf(repo1, repo2, repo3), 1L, 2L, {}, {}) + RepoChooser(listOf(repo1, repo2, repo3), 1L, 2L, null, {}, {}) } } diff --git a/next/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt index 00ea6ae95..8436ea34b 100644 --- a/next/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt @@ -11,6 +11,7 @@ import app.cash.molecule.AndroidUiDispatcher import app.cash.molecule.RecompositionMode.ContextClock import app.cash.molecule.launchMolecule import dagger.hilt.android.lifecycle.HiltViewModel +import io.ktor.client.engine.ProxyConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -41,7 +42,7 @@ class DiscoverViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val db: FDroidDatabase, updatesManager: UpdatesManager, - settingsManager: SettingsManager, + private val settingsManager: SettingsManager, private val repoManager: RepoManager, @param:IoDispatcher private val ioScope: CoroutineScope, ) : AndroidViewModel(app) { @@ -52,15 +53,17 @@ class DiscoverViewModel @Inject constructor( val numUpdates = updatesManager.numUpdates val newApps = db.getAppDao().getNewAppsFlow().map { list -> + val proxyConfig = settingsManager.proxyConfig list.mapNotNull { val repository = repoManager.getRepository(it.repoId) ?: return@mapNotNull null - it.toAppDiscoverItem(repository) + it.toAppDiscoverItem(repository, proxyConfig) } } val recentlyUpdatedApps = db.getAppDao().getRecentlyUpdatedAppsFlow().map { list -> + val proxyConfig = settingsManager.proxyConfig list.mapNotNull { val repository = repoManager.getRepository(it.repoId) ?: return@mapNotNull null - it.toAppDiscoverItem(repository) + it.toAppDiscoverItem(repository, proxyConfig) } } private val categories = db.getRepositoryDao().getLiveCategories().asFlow().map { categories -> @@ -108,6 +111,7 @@ class DiscoverViewModel @Inject constructor( log.info { "Searching for: $query" } val timedApps = measureTimedValue { try { + val proxyConfig = settingsManager.proxyConfig db.getAppDao().getAppSearchItems(query).sortedDescending().mapNotNull { val repository = repoManager.getRepository(it.repoId) ?: return@mapNotNull null AppListItem( @@ -117,7 +121,7 @@ class DiscoverViewModel @Inject constructor( summary = it.summary.getBestLocale(localeList) ?: "", lastUpdated = it.lastUpdated, isCompatible = true, // doesn't matter here, as we don't filter - iconModel = it.getIcon(localeList)?.getImageModel(repository), + iconModel = it.getIcon(localeList)?.getImageModel(repository, proxyConfig), categoryIds = it.categories?.toSet(), ) } @@ -144,10 +148,13 @@ class DiscoverViewModel @Inject constructor( searchResults.value = null } - private fun AppOverviewItem.toAppDiscoverItem(repository: Repository) = AppDiscoverItem( + private fun AppOverviewItem.toAppDiscoverItem( + repository: Repository, + proxyConfig: ProxyConfig?, + ) = AppDiscoverItem( packageName = packageName, name = getName(localeList) ?: "Unknown App", lastUpdated = lastUpdated, - imageModel = getIcon(localeList)?.getImageModel(repository), + imageModel = getIcon(localeList)?.getImageModel(repository, proxyConfig), ) } diff --git a/next/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt index 561aab9f5..42f0424ea 100644 --- a/next/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt @@ -57,8 +57,9 @@ class AppListViewModel @Inject constructor( }.sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) } } private val repositories = repoManager.repositoriesState.map { repositories -> + val proxyConfig = settingsManager.proxyConfig repositories.mapNotNull { - if (it.enabled) RepositoryItem(it, localeList) + if (it.enabled) RepositoryItem(it, localeList, proxyConfig) else null }.sortedBy { it.weight } } @@ -102,6 +103,7 @@ class AppListViewModel @Inject constructor( @WorkerThread private suspend fun loadApps(type: AppListType): List { val appDao = db.getAppDao() + val proxyConfig = settingsManager.proxyConfig return when (type) { is AppListType.Author -> appDao.getAppsByAuthor(type.authorName) is AppListType.Category -> appDao.getAppsByCategory(type.categoryId) @@ -119,7 +121,7 @@ class AppListViewModel @Inject constructor( summary = it.getSummary(localeList) ?: "Unknown", lastUpdated = it.lastUpdated, isCompatible = it.isCompatible, - iconModel = it.getIcon(localeList)?.getImageModel(repository), + iconModel = it.getIcon(localeList)?.getImageModel(repository, proxyConfig), categoryIds = it.categories?.toSet(), ) } diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/RepoIcon.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/RepoIcon.kt index a131b349b..e9d893433 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/RepoIcon.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/RepoIcon.kt @@ -4,15 +4,16 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.core.os.LocaleListCompat +import io.ktor.client.engine.ProxyConfig import org.fdroid.R import org.fdroid.database.Repository import org.fdroid.download.getImageModel import org.fdroid.ui.utils.AsyncShimmerImage @Composable -fun RepoIcon(repo: Repository, modifier: Modifier = Modifier) { +fun RepoIcon(repo: Repository, proxy: ProxyConfig?, modifier: Modifier = Modifier) { AsyncShimmerImage( - model = repo.getIcon(LocaleListCompat.getDefault())?.getImageModel(repo), + model = repo.getIcon(LocaleListCompat.getDefault())?.getImageModel(repo, proxy), contentDescription = null, error = painterResource(R.drawable.ic_repo_app_default), modifier = modifier, diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt index a053195c4..fe64391ae 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt @@ -66,7 +66,7 @@ class RepositoriesViewModel @Inject constructor( repos.update { repositories.mapNotNull { if (it.isArchiveRepo) null - else RepositoryItem(it, localeList) + else RepositoryItem(it, localeList, settingsManager.proxyConfig) } } repoSortingMap.update { diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoryItem.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoryItem.kt index 381bc2b57..95178cab7 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoryItem.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoryItem.kt @@ -1,6 +1,7 @@ package org.fdroid.ui.repositories import androidx.core.os.LocaleListCompat +import io.ktor.client.engine.ProxyConfig import org.fdroid.database.Repository import org.fdroid.download.getImageModel @@ -14,11 +15,11 @@ data class RepositoryItem( val weight: Int, val enabled: Boolean, ) { - constructor(repo: Repository, localeList: LocaleListCompat) : this( + constructor(repo: Repository, localeList: LocaleListCompat, proxy: ProxyConfig?) : this( repoId = repo.repoId, address = repo.address, name = repo.getName(localeList) ?: "Unknown Repo", - icon = repo.getIcon(localeList)?.getImageModel(repo), + icon = repo.getIcon(localeList)?.getImageModel(repo, proxy), timestamp = repo.timestamp, lastUpdated = repo.lastUpdated, weight = repo.weight, diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt index 0f5688b5a..e663c8bba 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.core.os.LocaleListCompat +import io.ktor.client.engine.ProxyConfig import org.fdroid.R import org.fdroid.index.IndexUpdateResult import org.fdroid.repo.AddRepoError @@ -30,6 +31,7 @@ import org.fdroid.repo.RepoUpdateWorker @Composable fun AddRepo( state: AddRepoState, + proxyConfig: ProxyConfig?, onFetchRepo: (String) -> Unit, onAddRepo: () -> Unit, onExistingRepo: (Long) -> Unit, @@ -73,6 +75,7 @@ fun AddRepo( } else { AddRepoPreviewScreen( state = state, + proxyConfig = proxyConfig, onAddRepo = onAddRepo, onExistingRepo = onExistingRepo, modifier = Modifier.padding(paddingValues), diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt index e7b5705c4..de22b8408 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt @@ -228,7 +228,7 @@ fun AddRepoIntroContent(onFetchRepo: (String) -> Unit, modifier: Modifier = Modi @Preview private fun Preview() { FDroidContent { - AddRepo(None, {}, {}, {}, { _, _ -> }) {} + AddRepo(None, null, {}, {}, {}, { _, _ -> }) {} } } @@ -236,6 +236,6 @@ private fun Preview() { @Preview(uiMode = UI_MODE_NIGHT_YES, widthDp = 720, heightDp = 360) private fun PreviewNight() { FDroidContent { - AddRepo(None, {}, {}, {}, { _, _ -> }) {} + AddRepo(None, null, {}, {}, {}, { _, _ -> }) {} } } diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoPreviewScreen.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoPreviewScreen.kt index b13e15546..a32b0d6f3 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoPreviewScreen.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoPreviewScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat +import io.ktor.client.engine.ProxyConfig import org.fdroid.R import org.fdroid.database.MinimalApp import org.fdroid.download.getImageModel @@ -34,6 +35,7 @@ import org.fdroid.ui.utils.getRepository @Composable fun AddRepoPreviewScreen( state: Fetching, + proxyConfig: ProxyConfig?, modifier: Modifier = Modifier, onAddRepo: () -> Unit, onExistingRepo: (Long) -> Unit, @@ -48,6 +50,7 @@ fun AddRepoPreviewScreen( item { RepoPreviewHeader( state = state, + proxyConfig = proxyConfig, onAddRepo = onAddRepo, onExistingRepo = onExistingRepo, modifier = Modifier @@ -88,7 +91,7 @@ fun AddRepoPreviewScreen( packageName = app.packageName, name = app.name ?: "Unknown app", summary = app.summary ?: "", - iconModel = app.getIcon(localeList)?.getImageModel(repo), + iconModel = app.getIcon(localeList)?.getImageModel(repo, proxyConfig), lastUpdated = 1L, isCompatible = true, ) @@ -137,6 +140,7 @@ private fun Preview() { FDroidContent(pureBlack = true) { AddRepoPreviewScreen( Fetching(address, repo, listOf(app1, app2, app3), IsNewRepository), + proxyConfig = null, onAddRepo = { }, onExistingRepo = {}, ) diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt index 1de9e87f3..542f634be 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt @@ -8,17 +8,21 @@ import kotlinx.coroutines.flow.StateFlow import mu.KotlinLogging import org.fdroid.index.RepoManager import org.fdroid.repo.AddRepoState +import org.fdroid.settings.SettingsManager import javax.inject.Inject @HiltViewModel class AddRepoViewModel @Inject constructor( app: Application, private val repoManager: RepoManager, + private val settingsManager: SettingsManager, ) : AndroidViewModel(app) { private val log = KotlinLogging.logger { } val state: StateFlow = repoManager.addRepoState + val proxyConfig = settingsManager.proxyConfig + override fun onCleared() { log.info { "onCleared() abort adding repository" } repoManager.abortAddingRepository() @@ -30,8 +34,7 @@ class AddRepoViewModel @Inject constructor( // TODO full only } else { repoManager.abortAddingRepository() - // TODO support proxy - repoManager.fetchRepositoryPreview(uri.toString(), proxy = null) + repoManager.fetchRepositoryPreview(uri.toString(), settingsManager.proxyConfig) } } diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/add/RepoPreviewHeader.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/add/RepoPreviewHeader.kt index c7df4589e..84a66fcb0 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/add/RepoPreviewHeader.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/add/RepoPreviewHeader.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat +import io.ktor.client.engine.ProxyConfig import org.fdroid.R import org.fdroid.fdroid.ui.theme.FDroidContent import org.fdroid.repo.FetchResult.IsExistingMirror @@ -40,6 +41,7 @@ import org.fdroid.ui.utils.getRepository @Composable fun RepoPreviewHeader( state: Fetching, + proxyConfig: ProxyConfig?, onAddRepo: () -> Unit, onExistingRepo: (Long) -> Unit, modifier: Modifier = Modifier, @@ -86,7 +88,7 @@ fun RepoPreviewHeader( horizontalArrangement = spacedBy(16.dp), verticalAlignment = CenterVertically, ) { - RepoIcon(repo, Modifier.size(48.dp)) + RepoIcon(repo, proxyConfig, Modifier.size(48.dp)) Column(horizontalAlignment = Alignment.Start) { Text( text = repo.getName(localeList) ?: "Unknown Repository", @@ -151,6 +153,7 @@ fun RepoPreviewScreenNewMirrorPreview() { onAddRepo = { }, onExistingRepo = {}, localeList = LocaleListCompat.getDefault(), + proxyConfig = null, ) } } @@ -170,6 +173,7 @@ fun RepoPreviewScreenNewRepoAndNewMirrorPreview() { onAddRepo = { }, onExistingRepo = {}, localeList = LocaleListCompat.getDefault(), + proxyConfig = null, ) } } @@ -185,6 +189,7 @@ fun RepoPreviewScreenExistingRepoPreview() { onAddRepo = { }, onExistingRepo = {}, localeList = LocaleListCompat.getDefault(), + proxyConfig = null, ) } } @@ -199,6 +204,7 @@ fun RepoPreviewScreenExistingMirrorPreview() { onAddRepo = { }, onExistingRepo = {}, localeList = LocaleListCompat.getDefault(), + proxyConfig = null, ) } } diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsContent.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsContent.kt index 035ab0161..75e12929e 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsContent.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsContent.kt @@ -38,7 +38,12 @@ fun RepoDetailsContent( .fillMaxWidth() .verticalScroll(rememberScrollState()), ) { - RepoDetailsHeader(repo, info.model.numberApps, onShowAppsClicked) + RepoDetailsHeader( + repo = repo, + numberOfApps = info.model.numberApps, + proxy = info.model.proxy, + onShowAppsClicked = onShowAppsClicked, + ) if (info.model.showOfficialMirrors) { OfficialMirrors( mirrors = info.model.officialMirrors, diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsHeader.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsHeader.kt index 2deecf1b2..5d6f97153 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsHeader.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsHeader.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat +import io.ktor.client.engine.ProxyConfig import org.fdroid.R import org.fdroid.database.Repository import org.fdroid.fdroid.ui.theme.FDroidContent @@ -33,6 +34,7 @@ import org.fdroid.ui.utils.getRepository fun RepoDetailsHeader( repo: Repository, numberOfApps: Int?, + proxy: ProxyConfig?, onShowAppsClicked: (String, Long) -> Unit, ) { val localeList = LocaleListCompat.getDefault() @@ -59,7 +61,7 @@ fun RepoDetailsHeader( Row( horizontalArrangement = spacedBy(8.dp), ) { - RepoIcon(repo, Modifier.size(64.dp)) + RepoIcon(repo, proxy, Modifier.size(64.dp)) Column(horizontalAlignment = Alignment.Start) { Text( text = name, @@ -110,6 +112,6 @@ fun RepoDetailsHeader( @Composable private fun Preview() { FDroidContent { - RepoDetailsHeader(getRepository(), 45) { _, _ -> } + RepoDetailsHeader(getRepository(), 45, null) { _, _ -> } } } diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt index af5010b72..2b68468d2 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.content.Intent.ACTION_SEND import android.content.Intent.EXTRA_TEXT import android.graphics.Bitmap +import io.ktor.client.engine.ProxyConfig import org.fdroid.R import org.fdroid.database.Repository import org.fdroid.download.Mirror @@ -39,6 +40,7 @@ data class RepoDetailsModel( val userMirrors: List, val archiveState: ArchiveState, val showOnboarding: Boolean, + val proxy: ProxyConfig?, ) { /** * The repo's address is currently also an official mirror. diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsPresenter.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsPresenter.kt index 11d7fd8c4..fa96d2f29 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsPresenter.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsPresenter.kt @@ -2,6 +2,7 @@ package org.fdroid.ui.repositories.details import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import io.ktor.client.engine.ProxyConfig import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import org.fdroid.database.Repository @@ -12,6 +13,7 @@ fun RepoDetailsPresenter( numAppsFlow: Flow, archiveStateFlow: StateFlow, showOnboardingFlow: StateFlow, + proxyConfig: ProxyConfig?, ): RepoDetailsModel { val repo = repoFlow.collectAsState(null).value return RepoDetailsModel( @@ -30,5 +32,6 @@ fun RepoDetailsPresenter( } ?: emptyList(), archiveState = archiveStateFlow.collectAsState().value, showOnboarding = showOnboardingFlow.collectAsState().value, + proxy = proxyConfig, ) } diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt index 85c112b54..a79bf0593 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt @@ -27,6 +27,7 @@ import org.fdroid.download.Mirror import org.fdroid.index.RepoManager import org.fdroid.repo.RepoUpdateWorker import org.fdroid.settings.OnboardingManager +import org.fdroid.settings.SettingsManager import org.fdroid.ui.repositories.details.ArchiveState.UNKNOWN import org.fdroid.utils.IoDispatcher import javax.inject.Inject @@ -36,6 +37,7 @@ class RepoDetailsViewModel @Inject constructor( app: Application, private val db: FDroidDatabase, private val repoManager: RepoManager, + private val settingsManager: SettingsManager, private val onboardingManager: OnboardingManager, @param:IoDispatcher private val ioScope: CoroutineScope, ) : AndroidViewModel(app), RepoDetailsActions { @@ -60,6 +62,7 @@ class RepoDetailsViewModel @Inject constructor( numAppsFlow = numAppsFlow, archiveStateFlow = archiveStateFlow, showOnboardingFlow = showOnboarding, + proxyConfig = settingsManager.proxyConfig, ) } diff --git a/next/src/main/kotlin/org/fdroid/ui/settings/PreferenceProxy.kt b/next/src/main/kotlin/org/fdroid/ui/settings/PreferenceProxy.kt new file mode 100644 index 000000000..478294700 --- /dev/null +++ b/next/src/main/kotlin/org/fdroid/ui/settings/PreferenceProxy.kt @@ -0,0 +1,128 @@ +package org.fdroid.ui.settings + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.VpnLock +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import me.zhanghai.compose.preference.ProvidePreferenceLocals +import me.zhanghai.compose.preference.textFieldPreference +import org.fdroid.R +import org.fdroid.fdroid.ui.theme.FDroidContent +import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PROXY +import org.fdroid.settings.SettingsConstants.PREF_KEY_PROXY +import java.net.InetSocketAddress + +fun LazyListScope.preferenceProxy( + proxyState: MutableState, + showError: MutableState, +) { + textFieldPreference( + key = PREF_KEY_PROXY, + defaultValue = PREF_DEFAULT_PROXY, + rememberState = { proxyState }, + icon = { + Icon( + imageVector = Icons.Default.VpnLock, + contentDescription = null, + ) + }, + title = { + Text(stringResource(R.string.pref_proxy_title)) + }, + summary = { + val value = proxyState.value + val s = if (value.isBlank()) { + stringResource(R.string.pref_proxy_disabled) + } else { + stringResource(R.string.pref_proxy_enabled, value) + } + Text(s) + }, + textToValue = { + if (it.isBlank() || isProxyValid(it)) { + showError.value = false + it + } else { + showError.value = true + // null is currently treated as an error and won't cause an update + null + } + }, + textField = { value, onValueChange, onOk -> + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + keyboardActions = KeyboardActions { onOk() }, + singleLine = true, + trailingIcon = { + if (value.text.isNotBlank()) { + IconButton(onClick = { onValueChange(TextFieldValue("")) }) { + Icon(Icons.Default.Clear, stringResource(R.string.clear)) + } + } + }, + isError = showError.value, + supportingText = { + val s = if (showError.value) { + stringResource(R.string.pref_proxy_error) + } else { + stringResource(R.string.pref_proxy_hint) + } + Text(s) + }, + ) + }, + ) +} + +private fun isProxyValid(proxyStr: String): Boolean = try { + val (host, port) = proxyStr.split(':') + InetSocketAddress.createUnresolved(host, port.toInt()) + true +} catch (_: Exception) { + false +} + +@Preview +@Composable +private fun PreviewDefault() { + FDroidContent { + ProvidePreferenceLocals { + val showProxyError = remember { mutableStateOf(false) } + val proxyState = remember { mutableStateOf(PREF_DEFAULT_PROXY) } + LazyColumn { + preferenceProxy(proxyState, showProxyError) + } + } + } +} + +@Preview +@Composable +private fun PreviewProxySet() { + FDroidContent { + ProvidePreferenceLocals { + val showProxyError = remember { mutableStateOf(false) } + val proxyState = remember { mutableStateOf("127.0.0.1:8000") } + LazyColumn { + preferenceProxy(proxyState, showProxyError) + } + } + } +} diff --git a/next/src/main/kotlin/org/fdroid/ui/settings/Settings.kt b/next/src/main/kotlin/org/fdroid/ui/settings/Settings.kt index 43f7f9f28..97aefe508 100644 --- a/next/src/main/kotlin/org/fdroid/ui/settings/Settings.kt +++ b/next/src/main/kotlin/org/fdroid/ui/settings/Settings.kt @@ -30,6 +30,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources @@ -45,12 +47,15 @@ import me.zhanghai.compose.preference.ProvidePreferenceLocals import me.zhanghai.compose.preference.listPreference import me.zhanghai.compose.preference.preference import me.zhanghai.compose.preference.preferenceCategory +import me.zhanghai.compose.preference.rememberPreferenceState import me.zhanghai.compose.preference.switchPreference import org.fdroid.R import org.fdroid.fdroid.ui.theme.FDroidContent import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_AUTO_UPDATES +import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PROXY import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_THEME import org.fdroid.settings.SettingsConstants.PREF_KEY_AUTO_UPDATES +import org.fdroid.settings.SettingsConstants.PREF_KEY_PROXY import org.fdroid.settings.SettingsConstants.PREF_KEY_THEME import org.fdroid.ui.utils.asRelativeTimeString import org.fdroid.utils.getLogName @@ -87,6 +92,8 @@ fun Settings( val context = LocalContext.current val res = LocalResources.current ProvidePreferenceLocals(prefsFlow) { + val showProxyError = remember { mutableStateOf(false) } + val proxyState = rememberPreferenceState(PREF_KEY_PROXY, PREF_DEFAULT_PROXY) LazyColumn( modifier = Modifier .padding(paddingValues) @@ -196,6 +203,11 @@ fun Settings( Text(s) }, ) + preferenceCategory( + key = "pref_category_network", + title = { Text(stringResource(R.string.pref_category_network)) }, + ) + preferenceProxy(proxyState, showProxyError) item { OutlinedButton( onClick = { launcher.launch("${getLogName(context)}.txt") }, diff --git a/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index c606324b5..44dda4c3a 100644 --- a/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -214,6 +214,7 @@ val testApp = AppDetailsItem( installedVersion = testVersion2, suggestedVersion = null, possibleUpdate = testVersion1, + proxy = null, ) fun getPreviewVersion(versionName: String, size: Long? = null) = object : PackageVersion { @@ -302,6 +303,7 @@ fun getRepoDetailsInfo( ), archiveState = ArchiveState.LOADING, showOnboarding = false, + proxy = null, ), ) = object : RepoDetailsInfo { override val model = model diff --git a/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt b/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt index a53069e79..32fe6e48e 100644 --- a/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt +++ b/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt @@ -19,6 +19,7 @@ import org.fdroid.database.FDroidDatabase import org.fdroid.download.getImageModel import org.fdroid.index.RepoManager import org.fdroid.install.AppInstallManager +import org.fdroid.settings.SettingsManager import org.fdroid.ui.apps.AppUpdateItem import org.fdroid.utils.IoDispatcher import javax.inject.Inject @@ -30,6 +31,7 @@ class UpdatesManager @Inject constructor( @ApplicationContext context: Context, private val db: FDroidDatabase, private val dbUpdateChecker: DbUpdateChecker, + private val settingsManager: SettingsManager, private val repoManager: RepoManager, private val appInstallManager: AppInstallManager, @param:IoDispatcher private val coroutineScope: CoroutineScope, @@ -70,6 +72,7 @@ class UpdatesManager @Inject constructor( val localeList = LocaleListCompat.getDefault() val updates = try { log.info { "Checking for updates..." } + val proxyConfig = settingsManager.proxyConfig dbUpdateChecker.getUpdatableApps(onlyFromPreferredRepo = true).map { update -> AppUpdateItem( repoId = update.repoId, @@ -79,7 +82,7 @@ class UpdatesManager @Inject constructor( update = update.update, whatsNew = update.update.getWhatsNew(localeList), iconModel = repoManager.getRepository(update.repoId)?.let { repo -> - update.getIcon(localeList)?.getImageModel(repo) + update.getIcon(localeList)?.getImageModel(repo, proxyConfig) }, ) } diff --git a/next/src/main/res/values/strings-next.xml b/next/src/main/res/values/strings-next.xml index 35f466984..cc9b64aa9 100644 --- a/next/src/main/res/values/strings-next.xml +++ b/next/src/main/res/values/strings-next.xml @@ -92,6 +92,7 @@ Filter Here you can apply filters to the list of apps, e.g. showing only apps within a certain category or repository. Changing the sort order is also possible. Got it + Clear Scanning the QR code can only work if you grant camera permission. Tap to grant it in settings. Last update: %s Username @@ -102,6 +103,12 @@ Opens system language settings Download and update apps daily when the device isn\'t being used + Network + Connect via SOCKS proxy + Connect to the internet without proxy + Uses proxy %s to connect + Proxy is expected in host:port format. + Proxy format invalid An unexpected error occurred. This is not your fault.