Skip to content

Commit c744ce9

Browse files
authored
Merge pull request #264 from rainxchzed/proxy-impl
2 parents a002a58 + 5362bc9 commit c744ce9

38 files changed

Lines changed: 1020 additions & 379 deletions

File tree

composeApp/src/androidMain/AndroidManifest.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
<uses-permission
1212
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
1313
tools:ignore="RequestInstallPackagesPolicy" />
14-
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
1514

1615
<application
1716
android:name=".app.GithubStoreApp"

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ import androidx.compose.foundation.layout.padding
88
import androidx.compose.material3.MaterialTheme
99
import androidx.compose.runtime.Composable
1010
import androidx.compose.runtime.CompositionLocalProvider
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.runtime.mutableStateOf
13+
import androidx.compose.runtime.remember
14+
import androidx.compose.runtime.setValue
1115
import androidx.compose.ui.Alignment
1216
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.layout.onGloballyPositioned
18+
import androidx.compose.ui.platform.LocalDensity
1319
import androidx.compose.ui.unit.dp
1420
import androidx.navigation.NavHostController
1521
import androidx.navigation.compose.NavHost
@@ -21,6 +27,7 @@ import org.koin.compose.viewmodel.koinViewModel
2127
import org.koin.core.parameter.parametersOf
2228
import zed.rainxch.apps.presentation.AppsRoot
2329
import zed.rainxch.auth.presentation.AuthenticationRoot
30+
import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
2431
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
2532
import zed.rainxch.details.presentation.DetailsRoot
2633
import zed.rainxch.devprofile.presentation.DeveloperProfileRoot
@@ -35,9 +42,12 @@ fun AppNavigation(
3542
navController: NavHostController
3643
) {
3744
val liquidState = rememberLiquidState()
45+
var bottomNavigationHeight by remember { mutableStateOf(0.dp) }
46+
val density = LocalDensity.current
3847

3948
CompositionLocalProvider(
40-
value = LocalBottomNavigationLiquid provides liquidState
49+
LocalBottomNavigationLiquid provides liquidState,
50+
LocalBottomNavigationHeight provides bottomNavigationHeight
4151
) {
4252
Box(
4353
modifier = Modifier.fillMaxSize()
@@ -235,6 +245,9 @@ fun AppNavigation(
235245
.align(Alignment.BottomCenter)
236246
.navigationBarsPadding()
237247
.padding(bottom = 24.dp)
248+
.onGloballyPositioned { coordinates ->
249+
bottomNavigationHeight = with(density) { coordinates.size.height.toDp() }
250+
}
238251
)
239252
}
240253
}

core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import java.net.PasswordAuthentication
1010
import java.net.ProxySelector
1111

1212
actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient {
13+
java.net.Authenticator.setDefault(null)
14+
1315
return HttpClient(OkHttp) {
1416
engine {
1517
when (proxyConfig) {

core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -180,20 +180,6 @@ class AndroidInstaller(
180180
}
181181
}
182182

183-
override fun uninstall(packageName: String) {
184-
Logger.d { "Requesting uninstall for: $packageName" }
185-
val intent = Intent(Intent.ACTION_DELETE).apply {
186-
data = "package:$packageName".toUri()
187-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
188-
}
189-
try {
190-
context.startActivity(intent)
191-
} catch (e: Exception) {
192-
Logger.w { "Failed to start uninstall for $packageName: ${e.message}" }
193-
}
194-
195-
}
196-
197183
override fun openApp(packageName: String): Boolean {
198184
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
199185
return if (launchIntent != null) {

core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import io.ktor.client.HttpClient
44
import kotlinx.coroutines.CoroutineScope
55
import kotlinx.coroutines.Dispatchers
66
import kotlinx.coroutines.SupervisorJob
7+
import kotlinx.coroutines.flow.first
8+
import kotlinx.coroutines.runBlocking
9+
import kotlinx.coroutines.withTimeout
710
import org.koin.dsl.module
811
import zed.rainxch.core.data.data_source.TokenStore
912
import zed.rainxch.core.data.data_source.impl.DefaultTokenStore
@@ -21,13 +24,16 @@ import zed.rainxch.core.data.repository.FavouritesRepositoryImpl
2124
import zed.rainxch.core.data.repository.InstalledAppsRepositoryImpl
2225
import zed.rainxch.core.data.repository.RateLimitRepositoryImpl
2326
import zed.rainxch.core.data.repository.StarredRepositoryImpl
27+
import zed.rainxch.core.data.repository.ProxyRepositoryImpl
2428
import zed.rainxch.core.data.repository.ThemesRepositoryImpl
2529
import zed.rainxch.core.domain.getPlatform
2630
import zed.rainxch.core.domain.logging.GitHubStoreLogger
2731
import zed.rainxch.core.domain.model.Platform
32+
import zed.rainxch.core.domain.model.ProxyConfig
2833
import zed.rainxch.core.domain.repository.AuthenticationState
2934
import zed.rainxch.core.domain.repository.FavouritesRepository
3035
import zed.rainxch.core.domain.repository.InstalledAppsRepository
36+
import zed.rainxch.core.domain.repository.ProxyRepository
3137
import zed.rainxch.core.domain.repository.RateLimitRepository
3238
import zed.rainxch.core.domain.repository.StarredRepository
3339
import zed.rainxch.core.domain.repository.ThemesRepository
@@ -84,6 +90,12 @@ val coreModule = module {
8490
)
8591
}
8692

93+
single<ProxyRepository> {
94+
ProxyRepositoryImpl(
95+
preferences = get()
96+
)
97+
}
98+
8799
single<SyncInstalledAppsUseCase> {
88100
SyncInstalledAppsUseCase(
89101
packageMonitor = get(),
@@ -96,6 +108,32 @@ val coreModule = module {
96108

97109
val networkModule = module {
98110
single<GitHubClientProvider> {
111+
val config = runBlocking {
112+
runCatching {
113+
withTimeout(1_500L) {
114+
get<ProxyRepository>().getProxyConfig().first()
115+
}
116+
}.getOrDefault(ProxyConfig.None)
117+
}
118+
119+
when (config) {
120+
is ProxyConfig.None -> ProxyManager.setNoProxy()
121+
is ProxyConfig.System -> ProxyManager.setSystemProxy()
122+
is ProxyConfig.Http -> ProxyManager.setHttpProxy(
123+
host = config.host,
124+
port = config.port,
125+
username = config.username,
126+
password = config.password
127+
)
128+
129+
is ProxyConfig.Socks -> ProxyManager.setSocksProxy(
130+
host = config.host,
131+
port = config.port,
132+
username = config.username,
133+
password = config.password
134+
)
135+
}
136+
99137
GitHubClientProvider(
100138
tokenStore = get(),
101139
rateLimitRepository = get(),

core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@ import kotlinx.coroutines.CoroutineScope
55
import kotlinx.coroutines.Dispatchers
66
import kotlinx.coroutines.SupervisorJob
77
import kotlinx.coroutines.cancel
8-
import kotlinx.coroutines.flow.Flow
9-
import kotlinx.coroutines.flow.MutableStateFlow
10-
import kotlinx.coroutines.flow.SharingStarted
118
import kotlinx.coroutines.flow.StateFlow
129
import kotlinx.coroutines.flow.distinctUntilChanged
13-
import kotlinx.coroutines.flow.map
14-
import kotlinx.coroutines.flow.stateIn
10+
import kotlinx.coroutines.flow.drop
11+
import kotlinx.coroutines.flow.launchIn
12+
import kotlinx.coroutines.flow.onEach
1513
import kotlinx.coroutines.sync.Mutex
1614
import kotlinx.coroutines.sync.withLock
1715
import zed.rainxch.core.data.data_source.TokenStore
@@ -24,37 +22,36 @@ class GitHubClientProvider(
2422
proxyConfigFlow: StateFlow<ProxyConfig>
2523
) {
2624
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
27-
private var currentClient: HttpClient? = null
2825
private val mutex = Mutex()
2926

30-
private val _client: StateFlow<HttpClient> = proxyConfigFlow
31-
.map { proxyConfig ->
32-
mutex.withLock {
33-
currentClient?.close()
34-
val newClient = createGitHubHttpClient(
35-
tokenStore = tokenStore,
36-
rateLimitRepository = rateLimitRepository,
37-
proxyConfig = proxyConfig
38-
)
39-
currentClient = newClient
40-
newClient
27+
@Volatile
28+
private var currentClient: HttpClient = createGitHubHttpClient(
29+
tokenStore = tokenStore,
30+
rateLimitRepository = rateLimitRepository,
31+
proxyConfig = proxyConfigFlow.value
32+
)
33+
34+
init {
35+
proxyConfigFlow
36+
.drop(1)
37+
.distinctUntilChanged()
38+
.onEach { proxyConfig ->
39+
mutex.withLock {
40+
currentClient.close()
41+
currentClient = createGitHubHttpClient(
42+
tokenStore = tokenStore,
43+
rateLimitRepository = rateLimitRepository,
44+
proxyConfig = proxyConfig
45+
)
46+
}
4147
}
42-
}
43-
.stateIn(
44-
scope = scope,
45-
started = SharingStarted.Eagerly,
46-
initialValue = createGitHubHttpClient(
47-
tokenStore = tokenStore,
48-
rateLimitRepository = rateLimitRepository,
49-
proxyConfig = proxyConfigFlow.value
50-
).also { currentClient = it }
51-
)
48+
.launchIn(scope)
49+
}
5250

53-
/** Get the current HttpClient (always up to date with proxy settings) */
54-
val client: HttpClient get() = _client.value
51+
val client: HttpClient get() = currentClient
5552

5653
fun close() {
57-
currentClient?.close()
54+
currentClient.close()
5855
scope.cancel()
5956
}
6057
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package zed.rainxch.core.data.repository
2+
3+
import androidx.datastore.core.DataStore
4+
import androidx.datastore.preferences.core.Preferences
5+
import androidx.datastore.preferences.core.edit
6+
import androidx.datastore.preferences.core.intPreferencesKey
7+
import androidx.datastore.preferences.core.stringPreferencesKey
8+
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.flow.map
10+
import zed.rainxch.core.data.network.ProxyManager
11+
import zed.rainxch.core.domain.model.ProxyConfig
12+
import zed.rainxch.core.domain.repository.ProxyRepository
13+
14+
class ProxyRepositoryImpl(
15+
private val preferences: DataStore<Preferences>
16+
) : ProxyRepository {
17+
18+
private val proxyTypeKey = stringPreferencesKey("proxy_type")
19+
private val proxyHostKey = stringPreferencesKey("proxy_host")
20+
private val proxyPortKey = intPreferencesKey("proxy_port")
21+
private val proxyUsernameKey = stringPreferencesKey("proxy_username")
22+
private val proxyPasswordKey = stringPreferencesKey("proxy_password")
23+
24+
override fun getProxyConfig(): Flow<ProxyConfig> {
25+
return preferences.data.map { prefs ->
26+
when (prefs[proxyTypeKey]) {
27+
"system" -> ProxyConfig.System
28+
"http" -> {
29+
val host = prefs[proxyHostKey]?.takeIf { it.isNotBlank() }
30+
val port = prefs[proxyPortKey]?.takeIf { it in 1..65535 }
31+
if (host != null && port != null) {
32+
ProxyConfig.Http(
33+
host = host,
34+
port = port,
35+
username = prefs[proxyUsernameKey],
36+
password = prefs[proxyPasswordKey]
37+
)
38+
} else {
39+
ProxyConfig.None
40+
}
41+
}
42+
"socks" -> {
43+
val host = prefs[proxyHostKey]?.takeIf { it.isNotBlank() }
44+
val port = prefs[proxyPortKey]?.takeIf { it in 1..65535 }
45+
if (host != null && port != null) {
46+
ProxyConfig.Socks(
47+
host = host,
48+
port = port,
49+
username = prefs[proxyUsernameKey],
50+
password = prefs[proxyPasswordKey]
51+
)
52+
} else {
53+
ProxyConfig.None
54+
}
55+
}
56+
else -> ProxyConfig.None
57+
}
58+
}
59+
}
60+
61+
override suspend fun setProxyConfig(config: ProxyConfig) {
62+
// Persist first so config survives crashes, then apply in-memory
63+
preferences.edit { prefs ->
64+
when (config) {
65+
is ProxyConfig.None -> {
66+
prefs[proxyTypeKey] = "none"
67+
prefs.remove(proxyHostKey)
68+
prefs.remove(proxyPortKey)
69+
prefs.remove(proxyUsernameKey)
70+
prefs.remove(proxyPasswordKey)
71+
}
72+
is ProxyConfig.System -> {
73+
prefs[proxyTypeKey] = "system"
74+
prefs.remove(proxyHostKey)
75+
prefs.remove(proxyPortKey)
76+
prefs.remove(proxyUsernameKey)
77+
prefs.remove(proxyPasswordKey)
78+
}
79+
is ProxyConfig.Http -> {
80+
prefs[proxyTypeKey] = "http"
81+
prefs[proxyHostKey] = config.host
82+
prefs[proxyPortKey] = config.port
83+
if (config.username != null) {
84+
prefs[proxyUsernameKey] = config.username!!
85+
} else {
86+
prefs.remove(proxyUsernameKey)
87+
}
88+
if (config.password != null) {
89+
prefs[proxyPasswordKey] = config.password!!
90+
} else {
91+
prefs.remove(proxyPasswordKey)
92+
}
93+
}
94+
is ProxyConfig.Socks -> {
95+
prefs[proxyTypeKey] = "socks"
96+
prefs[proxyHostKey] = config.host
97+
prefs[proxyPortKey] = config.port
98+
if (config.username != null) {
99+
prefs[proxyUsernameKey] = config.username!!
100+
} else {
101+
prefs.remove(proxyUsernameKey)
102+
}
103+
if (config.password != null) {
104+
prefs[proxyPasswordKey] = config.password!!
105+
} else {
106+
prefs.remove(proxyPasswordKey)
107+
}
108+
}
109+
}
110+
}
111+
applyToProxyManager(config)
112+
}
113+
114+
private fun applyToProxyManager(config: ProxyConfig) {
115+
when (config) {
116+
is ProxyConfig.None -> ProxyManager.setNoProxy()
117+
is ProxyConfig.System -> ProxyManager.setSystemProxy()
118+
is ProxyConfig.Http -> ProxyManager.setHttpProxy(
119+
host = config.host,
120+
port = config.port,
121+
username = config.username,
122+
password = config.password
123+
)
124+
is ProxyConfig.Socks -> ProxyManager.setSocksProxy(
125+
host = config.host,
126+
port = config.port,
127+
username = config.username,
128+
password = config.password
129+
)
130+
}
131+
}
132+
}

core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,6 @@ class DesktopInstaller(
5656

5757
}
5858

59-
override fun uninstall(packageName: String) {
60-
// Desktop doesn't have a unified uninstall mechanism
61-
Logger.d { "Uninstall not supported on desktop for: $packageName" }
62-
}
63-
6459
override fun openApp(packageName: String): Boolean {
6560
// Desktop apps are launched differently per platform
6661
Logger.d { "Open app not supported on desktop for: $packageName" }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package zed.rainxch.core.domain.repository
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import zed.rainxch.core.domain.model.ProxyConfig
5+
6+
interface ProxyRepository {
7+
fun getProxyConfig(): Flow<ProxyConfig>
8+
suspend fun setProxyConfig(config: ProxyConfig)
9+
}

0 commit comments

Comments
 (0)