Skip to content

Commit 308a867

Browse files
madeyeclaude
andcommitted
Replace raw Thread / Handler with coroutines + StateFlow
ProxyDroidVpnService now owns a SupervisorJob+Dispatchers.IO scope; the worker thread that built the tun device and started the Rust tun2socks is launched into that scope and cancelled in onDestroy. The `vpnInterface!!` deref is replaced with a local non-null after .establish(). ConnectivityBroadcastReceiver swaps Handler(Looper.getMainLooper()) + handler.post for BroadcastReceiver.goAsync() backed by a process-wide IO CoroutineScope. The handler body was split out and tightened with early-exits. Utils now exposes `working` and `connecting` as StateFlow<Boolean> in addition to the legacy @JvmStatic getters. MainViewModel collects those flows in viewModelScope, so the Compose connection card updates reactively instead of polling once per second. The polling loop in ProxyDroid.onCreate is gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ecd3235 commit 308a867

5 files changed

Lines changed: 215 additions & 230 deletions

File tree

Lines changed: 158 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,174 +1,158 @@
1-
/* proxydroid - Global / Individual Proxy App for Android
2-
* Copyright (C) 2011 Max Lv <max.c.lv@gmail.com>
3-
*
4-
* This program is free software: you can redistribute it and/or modify
5-
* it under the terms of the GNU General Public License as published by
6-
* the Free Software Foundation, either version 3 of the License, or
7-
* (at your option) any later version.
8-
*
9-
* This program is distributed in the hope that it will be useful,
10-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12-
* GNU General Public License for more details.
13-
*
14-
* You should have received a copy of the GNU General Public License
15-
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16-
*/
17-
18-
package org.proxydroid
19-
20-
import android.content.BroadcastReceiver
21-
import android.content.Context
22-
import android.content.Intent
23-
import android.net.ConnectivityManager
24-
import android.net.NetworkInfo
25-
import android.net.wifi.WifiManager
26-
import android.os.Handler
27-
import android.os.Looper
28-
import android.preference.PreferenceManager
29-
import com.ksmaze.android.preference.ListPreferenceMultiSelect
30-
import org.proxydroid.utils.Constraints
31-
import org.proxydroid.utils.Utils
32-
33-
class ConnectivityBroadcastReceiver : BroadcastReceiver() {
34-
35-
private val handler = Handler(Looper.getMainLooper())
36-
37-
companion object {
38-
private const val TAG = "ConnectivityBroadcastReceiver"
39-
}
40-
41-
override fun onReceive(context: Context, intent: Intent) {
42-
if (Utils.isConnecting()) return
43-
if (intent.action != ConnectivityManager.CONNECTIVITY_ACTION) return
44-
45-
handler.post {
46-
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
47-
val networkInfo = manager.activeNetworkInfo
48-
49-
if (networkInfo != null) {
50-
if (networkInfo.state == NetworkInfo.State.CONNECTING ||
51-
networkInfo.state == NetworkInfo.State.DISCONNECTING ||
52-
networkInfo.state == NetworkInfo.State.UNKNOWN
53-
) {
54-
return@post
55-
}
56-
} else {
57-
if (!Utils.isWorking()) return@post
58-
}
59-
60-
val settings = PreferenceManager.getDefaultSharedPreferences(context)
61-
val profile = Profile()
62-
profile.getProfile(settings)
63-
64-
// Store current settings first
65-
val oldProfile = settings.getString("profile", "1") ?: "1"
66-
settings.edit().putString(oldProfile, profile.toString()).apply()
67-
68-
// Load all profiles
69-
val profileValues = settings.getString("profileValues", "")?.split("\\|".toRegex()) ?: emptyList<String>()
70-
var curSSID: String? = null
71-
val lastSSID = settings.getString("lastSSID", "-1") ?: "-1"
72-
var autoConnect = false
73-
74-
// Test on each profile
75-
for (profileId in profileValues) {
76-
if (profileId.isEmpty()) continue
77-
val profileString = settings.getString(profileId, "") ?: ""
78-
profile.decodeJson(profileString)
79-
curSSID = onlineSSID(context, profile.ssid, profile.excludedSsid)
80-
if (profile.isAutoConnect && curSSID != null) {
81-
autoConnect = true
82-
settings.edit().putString("profile", profileId).apply()
83-
profile.setProfile(settings)
84-
break
85-
}
86-
}
87-
88-
if (networkInfo == null) {
89-
if (lastSSID != Constraints.ONLY_3G &&
90-
lastSSID != Constraints.WIFI_AND_3G &&
91-
lastSSID != Constraints.ONLY_WIFI
92-
) {
93-
if (Utils.isWorking()) {
94-
org.proxydroid.utils.ProxyController.stop(context)
95-
}
96-
}
97-
} else {
98-
if (networkInfo.state != NetworkInfo.State.CONNECTED) return@post
99-
100-
if (networkInfo.type == ConnectivityManager.TYPE_WIFI) {
101-
if (lastSSID != "-1") {
102-
val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
103-
val wInfo = wm.connectionInfo
104-
if (wInfo != null) {
105-
var current = wInfo.ssid
106-
current = current?.replace("\"", "")
107-
if (current != null && current != lastSSID) {
108-
if (Utils.isWorking()) {
109-
org.proxydroid.utils.ProxyController.stop(context)
110-
}
111-
}
112-
}
113-
}
114-
} else {
115-
if (lastSSID != Constraints.ONLY_3G && lastSSID != Constraints.WIFI_AND_3G) {
116-
if (Utils.isWorking()) {
117-
org.proxydroid.utils.ProxyController.stop(context)
118-
}
119-
}
120-
}
121-
}
122-
123-
if (autoConnect) {
124-
if (!Utils.isWorking()) {
125-
val pdr = ProxyDroidReceiver()
126-
settings.edit().putString("lastSSID", curSSID).apply()
127-
Utils.setConnecting(true)
128-
pdr.onReceive(context, intent)
129-
}
130-
}
131-
}
132-
}
133-
134-
fun onlineSSID(context: Context, ssid: String, excludedSsid: String): String? {
135-
val ssids = ListPreferenceMultiSelect.parseStoredValue(ssid) ?: return null
136-
val excludedSsids = ListPreferenceMultiSelect.parseStoredValue(excludedSsid)
137-
138-
if (ssids.isEmpty()) return null
139-
140-
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
141-
val networkInfo = manager.activeNetworkInfo ?: return null
142-
143-
if (networkInfo.type != ConnectivityManager.TYPE_WIFI) {
144-
for (item in ssids) {
145-
if (Constraints.WIFI_AND_3G == item) return item
146-
if (Constraints.ONLY_3G == item) return item
147-
}
148-
return null
149-
}
150-
151-
val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
152-
val wInfo = wm.connectionInfo
153-
if (wInfo?.ssid == null) return null
154-
155-
var current = wInfo.ssid
156-
if (current.isNullOrEmpty()) return null
157-
current = current.replace("\"", "")
158-
159-
if (excludedSsids != null) {
160-
for (item in excludedSsids) {
161-
if (current == item) {
162-
return null // Never connect proxy on excluded ssid
163-
}
164-
}
165-
}
166-
167-
for (item in ssids) {
168-
if (Constraints.WIFI_AND_3G == item) return item
169-
if (Constraints.ONLY_WIFI == item) return item
170-
if (current == item) return item
171-
}
172-
return null
173-
}
174-
}
1+
/* proxydroid - Global / Individual Proxy App for Android
2+
* Copyright (C) 2011 Max Lv <max.c.lv@gmail.com>
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*/
9+
10+
package org.proxydroid
11+
12+
import android.content.BroadcastReceiver
13+
import android.content.Context
14+
import android.content.Intent
15+
import android.net.ConnectivityManager
16+
import android.net.NetworkInfo
17+
import android.net.wifi.WifiManager
18+
import android.preference.PreferenceManager
19+
import com.ksmaze.android.preference.ListPreferenceMultiSelect
20+
import kotlinx.coroutines.CoroutineScope
21+
import kotlinx.coroutines.Dispatchers
22+
import kotlinx.coroutines.SupervisorJob
23+
import kotlinx.coroutines.launch
24+
import org.proxydroid.utils.Constraints
25+
import org.proxydroid.utils.ProxyController
26+
import org.proxydroid.utils.Utils
27+
28+
class ConnectivityBroadcastReceiver : BroadcastReceiver() {
29+
30+
override fun onReceive(context: Context, intent: Intent) {
31+
if (Utils.isConnecting()) return
32+
if (intent.action != ConnectivityManager.CONNECTIVITY_ACTION) return
33+
34+
// BroadcastReceiver runs on the main thread; offload to IO with goAsync().
35+
val pending = goAsync()
36+
scope.launch {
37+
try {
38+
handle(context.applicationContext, intent)
39+
} finally {
40+
pending.finish()
41+
}
42+
}
43+
}
44+
45+
private fun handle(context: Context, intent: Intent) {
46+
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
47+
val networkInfo = manager.activeNetworkInfo
48+
49+
if (networkInfo != null) {
50+
val state = networkInfo.state
51+
if (state == NetworkInfo.State.CONNECTING ||
52+
state == NetworkInfo.State.DISCONNECTING ||
53+
state == NetworkInfo.State.UNKNOWN
54+
) return
55+
} else if (!Utils.isWorking()) {
56+
return
57+
}
58+
59+
val settings = PreferenceManager.getDefaultSharedPreferences(context)
60+
val profile = Profile().apply { getProfile(settings) }
61+
62+
val oldProfile = settings.getString("profile", "1") ?: "1"
63+
settings.edit().putString(oldProfile, profile.toString()).apply()
64+
65+
val profileValues = settings.getString("profileValues", "")
66+
?.split("|")?.filter { it.isNotEmpty() }
67+
.orEmpty()
68+
var curSSID: String? = null
69+
val lastSSID = settings.getString("lastSSID", "-1") ?: "-1"
70+
var autoConnect = false
71+
72+
for (profileId in profileValues) {
73+
val profileString = settings.getString(profileId, "") ?: ""
74+
profile.decodeJson(profileString)
75+
curSSID = onlineSSID(context, profile.ssid, profile.excludedSsid)
76+
if (profile.isAutoConnect && curSSID != null) {
77+
autoConnect = true
78+
settings.edit().putString("profile", profileId).apply()
79+
profile.setProfile(settings)
80+
break
81+
}
82+
}
83+
84+
if (networkInfo == null) {
85+
if (lastSSID != Constraints.ONLY_3G &&
86+
lastSSID != Constraints.WIFI_AND_3G &&
87+
lastSSID != Constraints.ONLY_WIFI &&
88+
Utils.isWorking()
89+
) {
90+
ProxyController.stop(context)
91+
}
92+
} else {
93+
if (networkInfo.state != NetworkInfo.State.CONNECTED) return
94+
95+
if (networkInfo.type == ConnectivityManager.TYPE_WIFI) {
96+
if (lastSSID != "-1") {
97+
val wm = context.applicationContext
98+
.getSystemService(Context.WIFI_SERVICE) as WifiManager
99+
val current = wm.connectionInfo?.ssid?.replace("\"", "")
100+
if (current != null && current != lastSSID && Utils.isWorking()) {
101+
ProxyController.stop(context)
102+
}
103+
}
104+
} else if (lastSSID != Constraints.ONLY_3G &&
105+
lastSSID != Constraints.WIFI_AND_3G &&
106+
Utils.isWorking()
107+
) {
108+
ProxyController.stop(context)
109+
}
110+
}
111+
112+
if (autoConnect && !Utils.isWorking()) {
113+
settings.edit().putString("lastSSID", curSSID).apply()
114+
Utils.setConnecting(true)
115+
ProxyDroidReceiver().onReceive(context, intent)
116+
}
117+
}
118+
119+
private fun onlineSSID(context: Context, ssid: String, excludedSsid: String): String? {
120+
val ssids = ListPreferenceMultiSelect.parseStoredValue(ssid) ?: return null
121+
val excludedSsids = ListPreferenceMultiSelect.parseStoredValue(excludedSsid)
122+
123+
if (ssids.isEmpty()) return null
124+
125+
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
126+
val networkInfo = manager.activeNetworkInfo ?: return null
127+
128+
if (networkInfo.type != ConnectivityManager.TYPE_WIFI) {
129+
for (item in ssids) {
130+
if (Constraints.WIFI_AND_3G == item) return item
131+
if (Constraints.ONLY_3G == item) return item
132+
}
133+
return null
134+
}
135+
136+
val wm = context.applicationContext
137+
.getSystemService(Context.WIFI_SERVICE) as WifiManager
138+
val current = wm.connectionInfo?.ssid?.takeIf { it.isNotEmpty() }
139+
?.replace("\"", "") ?: return null
140+
141+
if (excludedSsids != null) {
142+
for (item in excludedSsids) {
143+
if (current == item) return null
144+
}
145+
}
146+
147+
for (item in ssids) {
148+
if (Constraints.WIFI_AND_3G == item) return item
149+
if (Constraints.ONLY_WIFI == item) return item
150+
if (current == item) return item
151+
}
152+
return null
153+
}
154+
155+
companion object {
156+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
157+
}
158+
}

app/src/main/java/org/proxydroid/ProxyDroid.kt

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@ import androidx.activity.result.contract.ActivityResultContracts
1919
import androidx.activity.viewModels
2020
import androidx.compose.runtime.collectAsState
2121
import androidx.compose.runtime.getValue
22-
import androidx.lifecycle.Lifecycle
23-
import androidx.lifecycle.lifecycleScope
24-
import androidx.lifecycle.repeatOnLifecycle
25-
import kotlinx.coroutines.delay
26-
import kotlinx.coroutines.launch
2722
import org.proxydroid.ui.MainScreen
2823
import org.proxydroid.ui.MainViewModel
2924
import org.proxydroid.ui.theme.ProxyDroidTheme
@@ -65,16 +60,8 @@ class ProxyDroid : ComponentActivity() {
6560
}
6661
}
6762

68-
// Light polling so the connection card reflects service-side flag changes
69-
// (VPN service flips Utils.isWorking from a worker thread).
70-
lifecycleScope.launch {
71-
repeatOnLifecycle(Lifecycle.State.STARTED) {
72-
while (true) {
73-
viewModel.refreshStatus()
74-
delay(1000)
75-
}
76-
}
77-
}
63+
// MainViewModel observes Utils.working / Utils.connecting StateFlows
64+
// directly, so the Compose UI auto-refreshes without polling.
7865

7966
if (intent?.getBooleanExtra(ProxyController.EXTRA_AUTO_START, false) == true) {
8067
startVpn()

0 commit comments

Comments
 (0)