Support for SOCKS proxy

adds a new setting and exposes the proxy in all those places where we do network requests
This commit is contained in:
Torsten Grote
2025-10-27 18:00:06 -03:00
parent 43162b0b07
commit 54e3ad4d8a
32 changed files with 285 additions and 44 deletions

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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

View File

@@ -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 ->

View File

@@ -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<AppListItem>> { 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)
},
)
}

View File

@@ -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),

View File

@@ -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<AntiFeature>? {
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),
)

View File

@@ -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,
)
}

View File

@@ -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<AppInfo?>,
@@ -190,6 +192,7 @@ fun DetailsPresenter(
noUpdatesBecauseDifferentSigner = noUpdatesBecauseDifferentSigner,
authorHasMoreThanOneApp = authorHasMoreThanOneApp,
localeList = locales,
proxy = settingsManager.proxyConfig,
)
}

View File

@@ -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<Repository>,
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, {}, {})
}
}

View File

@@ -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),
)
}

View File

@@ -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<AppListItem> {
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(),
)
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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),

View File

@@ -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, {}, {}, {}, { _, _ -> }) {}
}
}

View File

@@ -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 = {},
)

View File

@@ -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<AddRepoState> = 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)
}
}

View File

@@ -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,
)
}
}

View File

@@ -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,

View File

@@ -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) { _, _ -> }
}
}

View File

@@ -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<UserMirrorItem>,
val archiveState: ArchiveState,
val showOnboarding: Boolean,
val proxy: ProxyConfig?,
) {
/**
* The repo's address is currently also an official mirror.

View File

@@ -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<Int?>,
archiveStateFlow: StateFlow<ArchiveState>,
showOnboardingFlow: StateFlow<Boolean>,
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,
)
}

View File

@@ -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,
)
}

View File

@@ -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<String>,
showError: MutableState<Boolean>,
) {
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)
}
}
}
}

View File

@@ -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") },

View File

@@ -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

View File

@@ -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)
},
)
}

View File

@@ -92,6 +92,7 @@
<string name="onboarding_app_list_filter_title">Filter</string>
<string name="onboarding_app_list_filter_message">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.</string>
<string name="got_it">Got it</string>
<string name="clear">Clear</string>
<string name="permission_camera_denied">Scanning the QR code can only work if you grant camera permission. Tap to grant it in settings.</string>
<string name="repo_last_update_upstream">Last update: %s</string>
<string name="repo_basicauth_username">Username</string>
@@ -102,6 +103,12 @@
<string name="pref_language_summary">Opens system language settings</string>
<string name="pref_auto_updates_summary">Download and update apps daily when the device isn\'t being used</string>
<string name="pref_category_network">Network</string>
<string name="pref_proxy_title">Connect via SOCKS proxy</string>
<string name="pref_proxy_disabled">Connect to the internet without proxy</string>
<string name="pref_proxy_enabled">Uses proxy %s to connect</string>
<string name="pref_proxy_hint">Proxy is expected in host:port format.</string>
<string name="pref_proxy_error">Proxy format invalid</string>
<string name="crash_report_text">An unexpected error occurred.
This is not your fault.