Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
470 changes: 469 additions & 1 deletion app/src/main/java/org/monogram/app/MainContent.kt
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, you need to split the code into components and move it to app/components

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
<string name="proxy_port">Порт</string>
<string name="proxy_type">Тип</string>
<string name="proxy_unknown">Неизвестно</string>
<string name="proxy_saved_vpn_not_enabled">Прокси сохранено, но не включено из-за активной VPN</string>
<string name="proxy_enable_failed_vpn_active">Невозможно включить прокси: VPN активна</string>
<string name="proxy_added_and_enabled">Прокси добавлен и включён</string>
<string name="cancel">Отмена</string>
<string name="connect">Подключиться</string>
<string name="save">Сохранить</string>
<string name="proxy_save_for_later">Сохранить на потом</string>

<!-- Main Content / Chat Confirm -->
<string name="chat_join">Присоединиться</string>
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values-sk/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
<string name="proxy_port">Port</string>
<string name="proxy_type">Typ</string>
<string name="proxy_unknown">Neznáme</string>
<string name="proxy_saved_vpn_not_enabled">Proxy uložené, ale neaktivované kvôli aktívnej VPN</string>
<string name="proxy_enable_failed_vpn_active">Nemožno aktivovať proxy: VPN je aktívna</string>
<string name="proxy_added_and_enabled">Proxy pridaný a aktivovaný</string>
<string name="cancel">Zrušiť</string>
<string name="connect">Pripojiť</string>
<string name="save">Uložiť</string>
<string name="proxy_save_for_later">Uložiť na neskôr</string>

<!-- Main Content / Chat Confirm -->
<string name="chat_join">Pripojiť sa</string>
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values-uk/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
<string name="proxy_port">Порт</string>
<string name="proxy_type">Тип</string>
<string name="proxy_unknown">Невідомо</string>
<string name="proxy_saved_vpn_not_enabled">Проксі збережено, але не увімкнено через активну VPN</string>
<string name="proxy_enable_failed_vpn_active">Неможливо увімкнути проксі: VPN активна</string>
<string name="proxy_added_and_enabled">Проксі додано та увімкнено</string>
<string name="cancel">Скасувати</string>
<string name="connect">Підключитися</string>
<string name="save">Зберегти</string>
<string name="proxy_save_for_later">Зберегти на потім</string>

<!-- Main Content / Chat Confirm -->
<string name="chat_join">Приєднатися</string>
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values-zh/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
<string name="proxy_port">端口</string>
<string name="proxy_type">类型</string>
<string name="proxy_unknown">未知</string>
<string name="proxy_saved_vpn_not_enabled">代理已保存,但因 VPN 处于活动状态而未启用</string>
<string name="proxy_enable_failed_vpn_active">无法启用代理:VPN 处于活动状态</string>
<string name="proxy_added_and_enabled">代理已添加并启用</string>
<string name="cancel">取消</string>
<string name="connect">连接</string>
<string name="save">保存</string>
<string name="proxy_save_for_later">稍后保存</string>

<!-- Main Content / Chat Confirm -->
<string name="chat_join">加入</string>
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
<string name="proxy_port">Port</string>
<string name="proxy_type">Type</string>
<string name="proxy_unknown">Unknown</string>
<string name="proxy_saved_vpn_not_enabled">Proxy saved but not enabled due to active VPN</string>
<string name="proxy_enable_failed_vpn_active">Cannot enable proxy: VPN is active</string>
<string name="proxy_added_and_enabled">Proxy added and enabled</string>
<string name="cancel">Cancel</string>
<string name="connect">Connect</string>
<string name="save">Save</string>
<string name="proxy_save_for_later">Save for Later</string>

<!-- Main Content / Chat Confirm -->
<string name="chat_join">Join</string>
Expand Down
14 changes: 11 additions & 3 deletions data/src/main/java/org/monogram/data/di/dataModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.monogram.data.gateway.TelegramGateway
import org.monogram.data.gateway.TelegramGatewayImpl
import org.monogram.data.gateway.UpdateDispatcher
import org.monogram.data.gateway.UpdateDispatcherImpl
import org.monogram.domain.infra.VpnDetector as VpnDetectorInterface
import org.monogram.data.infra.*
import org.monogram.data.mapper.ChatMapper
import org.monogram.data.mapper.MessageMapper
Expand Down Expand Up @@ -181,7 +182,7 @@ val dataModule = module {
single<ChatRemoteSource> {
TdChatRemoteSource(
gateway = get(),
connectivityManager = androidContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager,
connectivityManager = androidContext().getSystemService(ConnectivityManager::class.java),
)
}

Expand Down Expand Up @@ -215,7 +216,7 @@ val dataModule = module {

single {
MessageMapper(
connectivityManager = androidContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager,
connectivityManager = androidContext().getSystemService(ConnectivityManager::class.java),
gateway = get(),
userRepository = get(),
customEmojiPaths = get<FileUpdateHandler>().customEmojiPaths,
Expand All @@ -227,14 +228,21 @@ val dataModule = module {
)
}

single<VpnDetectorInterface> {
VpnDetector(
connectivityManager = androidContext().getSystemService(ConnectivityManager::class.java)
)
}

single {
ConnectionManager(
chatRemoteSource = get(),
proxyRemoteSource = get(),
updates = get(),
appPreferences = get(),
dispatchers = get(),
connectivityManager = androidContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager,
connectivityManager = androidContext().getSystemService(ConnectivityManager::class.java),
vpnDetector = get(),
scopeProvider = get()
)
}
Expand Down
99 changes: 94 additions & 5 deletions data/src/main/java/org/monogram/data/infra/ConnectionManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.monogram.core.ScopeProvider
import org.monogram.data.datasource.remote.ChatRemoteSource
import org.monogram.data.datasource.remote.ProxyRemoteDataSource
import org.monogram.data.gateway.UpdateDispatcher
import org.monogram.domain.infra.VpnDetector
import org.monogram.domain.repository.AppPreferencesProvider
import org.monogram.domain.repository.ConnectionStatus
import kotlin.random.Random
Expand All @@ -29,16 +30,24 @@ class ConnectionManager(
private val appPreferences: AppPreferencesProvider,
private val dispatchers: DispatcherProvider,
private val connectivityManager: ConnectivityManager,
private val vpnDetector: VpnDetector,
scopeProvider: ScopeProvider
) {
private val TAG = "ConnectionManager"
private val scope = scopeProvider.appScope

private data class ProxyModeState(
val autoBest: Boolean,
val telega: Boolean,
val vpnBlocking: Boolean
)

private val _connectionStateFlow = MutableStateFlow<ConnectionStatus>(ConnectionStatus.Connecting)
val connectionStateFlow = _connectionStateFlow.asStateFlow()

private var retryJob: Job? = null
private var proxyModeWatcherJob: Job? = null
private var vpnMonitoringJob: Job? = null
private var autoBestJob: Job? = null
private var telegaSwitchJob: Job? = null
private var watchdogJob: Job? = null
Expand Down Expand Up @@ -67,13 +76,61 @@ class ConnectionManager(
registerNetworkCallback()
startWatchdog()
startProxyManagement()
startVpnMonitoring()

scope.launch(dispatchers.default) {
runReconnectAttempt("bootstrap", force = true)
syncConnectionStateFromTdlib("bootstrap")
}
}

private fun startVpnMonitoring() {
vpnMonitoringJob?.cancel()
vpnDetector.stopMonitoring()
vpnDetector.startMonitoring()

vpnMonitoringJob = scope.launch {
combine(
appPreferences.isVpnAutoDisableEnabled,
vpnDetector.isVpnActive
) { enabled, vpnActive -> enabled to vpnActive }
.distinctUntilChanged()
.collect { (enabled, vpnActive) ->
if (!enabled) return@collect

if (vpnActive) {
val currentProxyId = appPreferences.enabledProxyId.value
if (currentProxyId != null) {
appPreferences.setSavedProxyBeforeVpn(currentProxyId)
Log.d(TAG, "VPN detected active, disabling in-app proxy (saved proxy: $currentProxyId)")
coRunCatching {
proxyRemoteSource.disableProxy()
appPreferences.setEnabledProxyId(null)
}.onFailure { Log.e(TAG, "Failed to disable proxy for VPN", it) }
}
autoBestJob?.cancel()
telegaSwitchJob?.cancel()
} else {
val savedProxyId = appPreferences.savedProxyBeforeVpn.value
if (savedProxyId != null) {
Log.d(TAG, "VPN disconnected, restoring proxy: $savedProxyId")
coRunCatching {
if (proxyRemoteSource.enableProxy(savedProxyId)) {
appPreferences.setEnabledProxyId(savedProxyId)
appPreferences.setSavedProxyBeforeVpn(null)
} else {
Log.w(TAG, "enableProxy returned false for saved proxy $savedProxyId, clearing and letting auto-best handle it")
appPreferences.setSavedProxyBeforeVpn(null)
}
}.onFailure { Log.e(TAG, "Failed to restore proxy after VPN", it) }
}
// Do NOT restart jobs here — startProxyManagement owns job lifecycle
// and will restart them reactively when VPN state changes.
}
}
}
}

private fun handleConnectionState(state: TdApi.ConnectionState, source: String) {
val status = when (state) {
is TdApi.ConnectionStateReady -> ConnectionStatus.Connected
Expand Down Expand Up @@ -215,16 +272,25 @@ class ConnectionManager(

combine(
appPreferences.isAutoBestProxyEnabled,
appPreferences.isTelegaProxyEnabled
) { autoBest, telega -> autoBest to telega }
appPreferences.isTelegaProxyEnabled,
appPreferences.isVpnAutoDisableEnabled,
vpnDetector.isVpnActive
) { autoBest, telega, vpnEnabled, vpnActive ->
ProxyModeState(autoBest, telega, vpnEnabled && vpnActive)
}
.distinctUntilChanged()
.collect { (autoBest, telega) ->
.collect { modeState ->
autoBestJob?.cancel()
telegaSwitchJob?.cancel()

if (telega) {
if (modeState.vpnBlocking) {
// VPN is active and auto-disable is enabled — do not start proxy jobs
return@collect
}

if (modeState.telega) {
telegaSwitchJob = launchTelegaSwitchLoop()
} else if (autoBest) {
} else if (modeState.autoBest) {
autoBestJob = launchAutoBestLoop()
}
}
Expand All @@ -248,6 +314,11 @@ class ConnectionManager(
}

private suspend fun selectBestProxy(telegaOnly: Boolean = false) {
if (appPreferences.isVpnAutoDisableEnabled.value && vpnDetector.isVpnActive.value) {
Log.d(TAG, "Skipping proxy selection — VPN is active")
return
}

val allProxies = proxyRemoteSource.getProxies()
val proxies = if (telegaOnly) {
val telegaIds = getTelegaIdentifiers()
Expand Down Expand Up @@ -281,6 +352,10 @@ class ConnectionManager(
val currentEnabled = proxies.find { it.isEnabled }
if (best.first.id != currentEnabled?.id) {
Log.d(TAG, "Switching to better proxy: ${best.first.server}:${best.first.port} (ping: ${best.second}ms)")
if (appPreferences.isVpnAutoDisableEnabled.value && vpnDetector.isVpnActive.value) {
Log.d(TAG, "VPN became active during proxy selection, aborting")
return
}
if (proxyRemoteSource.enableProxy(best.first.id)) {
appPreferences.setEnabledProxyId(best.first.id)
}
Expand Down Expand Up @@ -353,6 +428,20 @@ class ConnectionManager(
}
}


fun close() {
vpnDetector.stopMonitoring()
vpnMonitoringJob?.cancel()
retryJob?.cancel()
proxyModeWatcherJob?.cancel()
autoBestJob?.cancel()
telegaSwitchJob?.cancel()
watchdogJob?.cancel()
networkCallback?.let {
runCatching { connectivityManager.unregisterNetworkCallback(it) }
networkCallback = null
}
}
private fun onNetworkChanged(reason: String) {
scope.launch(dispatchers.default) {
runReconnectAttempt("network_$reason", force = true)
Expand Down
92 changes: 92 additions & 0 deletions data/src/main/java/org/monogram/data/infra/VpnDetector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.monogram.data.infra

import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.SystemClock
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.monogram.domain.infra.VpnDetector as VpnDetectorInterface

class VpnDetector(
private val connectivityManager: ConnectivityManager
) : VpnDetectorInterface {
private val TAG = "VpnDetector"
private val _isVpnActive = MutableStateFlow(false)
override val isVpnActive: StateFlow<Boolean> = _isVpnActive.asStateFlow()

private var networkCallback: ConnectivityManager.NetworkCallback? = null
private var lastCheckAtElapsedMs = 0L
private val minCheckIntervalMs = 500L

override fun startMonitoring() {
if (networkCallback != null) return

checkVpnStatus()

val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
checkVpnStatus()
}

override fun onLost(network: Network) {
checkVpnStatus()
}

override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
if (connectivityManager.activeNetwork == network) {
checkVpnStatus()
}
}
}

runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectivityManager.registerDefaultNetworkCallback(callback)
} else {
val request = NetworkRequest.Builder().build()
connectivityManager.registerNetworkCallback(request, callback)
}
networkCallback = callback
}.onFailure { throwable ->
Log.e(TAG, "Failed to register network callback -- VPN detection disabled", throwable)
}
}

override fun stopMonitoring() {
networkCallback?.let {
runCatching { connectivityManager.unregisterNetworkCallback(it) }
networkCallback = null
}
}

private fun checkVpnStatus() {
val now = SystemClock.elapsedRealtime()
if (now - lastCheckAtElapsedMs < minCheckIntervalMs) return
lastCheckAtElapsedMs = now

val isActive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val activeNetwork = connectivityManager.activeNetwork
if (activeNetwork != null) {
val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
} else {
false
}
} else {
@Suppress("DEPRECATION")
connectivityManager.activeNetworkInfo?.type == ConnectivityManager.TYPE_VPN
}

if (_isVpnActive.value != isActive) {
_isVpnActive.value = isActive
}
}
}
Loading