mirror of
https://github.com/cherepavel/VPN-Detector.git
synced 2026-08-18 05:24:00 +00:00
убрано фоновое отслеживание, приложение переведено на manual refresh
This commit is contained in:
parent
b036ee8400
commit
356bca8d26
5 changed files with 13 additions and 285 deletions
|
|
@ -3,10 +3,6 @@ package com.cherepavel.vpndetector
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
|
|
@ -35,7 +31,6 @@ import com.cherepavel.vpndetector.ui.SignalState
|
|||
import com.cherepavel.vpndetector.util.nowString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.OutputStreamWriter
|
||||
|
|
@ -96,24 +91,6 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private var lastExportText: String = ""
|
||||
private var detectionJob: Job? = null
|
||||
private var scheduledRefreshJob: Job? = null
|
||||
private var networkCallbackRegistered = false
|
||||
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) = scheduleRefresh()
|
||||
|
||||
override fun onLost(network: Network) = scheduleRefresh()
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) = scheduleRefresh()
|
||||
|
||||
override fun onLinkPropertiesChanged(
|
||||
network: Network,
|
||||
linkProperties: LinkProperties
|
||||
) = scheduleRefresh()
|
||||
}
|
||||
|
||||
private val createDocumentLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||
|
|
@ -140,15 +117,12 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
bindViews()
|
||||
setupListeners()
|
||||
registerNetworkCallback()
|
||||
lifecycleScope.launch(Dispatchers.IO) { TrackedAppsRepository.refresh(applicationContext) }
|
||||
refreshUi()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterNetworkCallback()
|
||||
detectionJob?.cancel()
|
||||
scheduledRefreshJob?.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
|
@ -245,32 +219,6 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun scheduleRefresh() {
|
||||
scheduledRefreshJob?.cancel()
|
||||
scheduledRefreshJob = lifecycleScope.launch {
|
||||
delay(250)
|
||||
refreshUi()
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerNetworkCallback() {
|
||||
if (networkCallbackRegistered) return
|
||||
val request = NetworkRequest.Builder().build()
|
||||
runCatching {
|
||||
connectivityManager.registerNetworkCallback(request, networkCallback)
|
||||
}.onSuccess {
|
||||
networkCallbackRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterNetworkCallback() {
|
||||
if (!networkCallbackRegistered) return
|
||||
runCatching {
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
networkCallbackRegistered = false
|
||||
}
|
||||
|
||||
private fun runDetection(): DetectionOutput {
|
||||
val snapshot = detectionEngine.detect()
|
||||
val report = ReportFormatter.build(snapshot)
|
||||
|
|
|
|||
|
|
@ -1,166 +0,0 @@
|
|||
package com.cherepavel.vpndetector.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.cherepavel.vpndetector.MainActivity
|
||||
import com.cherepavel.vpndetector.R
|
||||
import com.cherepavel.vpndetector.detector.DetectionEngine
|
||||
import com.cherepavel.vpndetector.detector.IDetectionEngine
|
||||
import com.cherepavel.vpndetector.model.DetectionSnapshot
|
||||
import com.cherepavel.vpndetector.model.DetectionStatus
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class VpnMonitorService : Service() {
|
||||
|
||||
private val connectivityManager by lazy {
|
||||
getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
}
|
||||
private val detectionEngine: IDetectionEngine by lazy { DetectionEngine(this) }
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
private var networkCallbackRegistered = false
|
||||
private var debounceJob: Job? = null
|
||||
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) = scheduleDetection()
|
||||
override fun onLost(network: Network) = scheduleDetection()
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) = scheduleDetection()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startForeground(NOTIFICATION_ID, buildNotification("Monitoring VPN state…"))
|
||||
registerNetworkCallback()
|
||||
scheduleDetection()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterNetworkCallback()
|
||||
scope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun scheduleDetection() {
|
||||
debounceJob?.cancel()
|
||||
debounceJob = scope.launch {
|
||||
delay(DEBOUNCE_MS)
|
||||
val snapshot = withContext(Dispatchers.IO) { detectionEngine.detect() }
|
||||
updateNotification(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification(snapshot: DetectionSnapshot) {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.notify(NOTIFICATION_ID, buildNotification(snapshot.toStatusText()))
|
||||
}
|
||||
|
||||
private fun DetectionSnapshot.toStatusText(): String = when (assessment.status) {
|
||||
DetectionStatus.ACTIVE_VPN ->
|
||||
if (lockdownLikely) "VPN active — lockdown mode" else "VPN active"
|
||||
DetectionStatus.SPLIT_TUNNEL ->
|
||||
"VPN present (split tunnel / bypass)"
|
||||
DetectionStatus.VPN_LIKE ->
|
||||
"VPN-like signals detected (score ${assessment.score}/100)"
|
||||
DetectionStatus.APPS_PRESENT ->
|
||||
"No active VPN — VPN apps installed"
|
||||
DetectionStatus.NO_EVIDENCE ->
|
||||
"No VPN detected"
|
||||
}
|
||||
|
||||
private fun buildNotification(text: String): Notification {
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0,
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("VPN Detector")
|
||||
.setContentText(text)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"VPN Monitor",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Live VPN detection status"
|
||||
setShowBadge(false)
|
||||
}
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
|
||||
.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerNetworkCallback() {
|
||||
if (networkCallbackRegistered) return
|
||||
runCatching {
|
||||
connectivityManager.registerNetworkCallback(
|
||||
NetworkRequest.Builder().build(),
|
||||
networkCallback
|
||||
)
|
||||
}.onSuccess { networkCallbackRegistered = true }
|
||||
}
|
||||
|
||||
private fun unregisterNetworkCallback() {
|
||||
if (!networkCallbackRegistered) return
|
||||
runCatching { connectivityManager.unregisterNetworkCallback(networkCallback) }
|
||||
networkCallbackRegistered = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
private const val CHANNEL_ID = "vpn_monitor_channel"
|
||||
private const val DEBOUNCE_MS = 300L
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, VpnMonitorService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, VpnMonitorService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
</manifest>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
|
|
|
|||
|
|
@ -3,51 +3,26 @@ package com.cherepavel.vpndetector.detector
|
|||
import android.content.Context
|
||||
import com.cherepavel.vpndetector.model.TrackedApp
|
||||
import org.json.JSONArray
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import androidx.core.content.edit
|
||||
|
||||
object TrackedAppsRepository {
|
||||
|
||||
private const val APPS_URL =
|
||||
"https://raw.githubusercontent.com/cherepavel/VPN-Detector/main/tracked_apps.json"
|
||||
private const val PREFS_NAME = "vpn_detector"
|
||||
private const val PREFS_KEY = "tracked_apps_json"
|
||||
private const val TIMEOUT_MS = 5_000
|
||||
@Volatile
|
||||
private var cached: List<TrackedApp>? = null
|
||||
|
||||
@Volatile private var cached: List<TrackedApp>? = null
|
||||
private const val FILE_NAME = "tracked_apps.json"
|
||||
|
||||
/** Fetch from remote and update cache. Call from a background thread. */
|
||||
fun refresh(context: Context) {
|
||||
try {
|
||||
val json = fetch()
|
||||
val apps = parse(json)
|
||||
cached = apps
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit { putString(PREFS_KEY, json) }
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Return cached → SharedPreferences → bundled fallback. */
|
||||
fun get(context: Context): List<TrackedApp> {
|
||||
cached?.let { return it }
|
||||
val stored = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getString(PREFS_KEY, null)
|
||||
if (stored != null) {
|
||||
return parse(stored).also { cached = it }
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
private fun fetch(): String {
|
||||
val connection = URL(APPS_URL).openConnection() as HttpURLConnection
|
||||
connection.connectTimeout = TIMEOUT_MS
|
||||
connection.readTimeout = TIMEOUT_MS
|
||||
return try {
|
||||
connection.inputStream.bufferedReader().use { it.readText() }
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
val json = context.applicationContext.assets
|
||||
.open(FILE_NAME)
|
||||
.bufferedReader()
|
||||
.use { it.readText() }
|
||||
|
||||
parse(json).also { cached = it }
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,30 +36,5 @@ object TrackedAppsRepository {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val fallback = listOf(
|
||||
TrackedApp("com.github.dyhkwong.sagernet", "ExclaveVPN"),
|
||||
TrackedApp("com.v2ray.ang", "v2rayNG"),
|
||||
TrackedApp("org.amnezia.awg", "AmneziaWG"),
|
||||
TrackedApp("org.amnezia.vpn", "Amnezia VPN"),
|
||||
TrackedApp("de.blinkt.openvpn", "OpenVPN for Android"),
|
||||
TrackedApp("net.openvpn.openvpn", "OpenVPN Connect"),
|
||||
TrackedApp("com.wireguard.android", "WireGuard"),
|
||||
TrackedApp("com.cloudflare.onedotonedotonedotone", "Cloudflare WARP"),
|
||||
TrackedApp("com.psiphon3", "Psiphon"),
|
||||
TrackedApp("app.hiddify.com", "Hiddify"),
|
||||
TrackedApp("io.nekohasekai.sfa", "SFA"),
|
||||
TrackedApp("com.nordvpn.android", "NordVPN"),
|
||||
TrackedApp("com.expressvpn.vpn", "ExpressVPN"),
|
||||
TrackedApp("com.protonvpn.android", "Proton VPN"),
|
||||
TrackedApp("ch.protonvpn.android", "Proton VPN (legacy package)"),
|
||||
TrackedApp("free.vpn.unblock.proxy.turbovpn", "Turbo VPN"),
|
||||
TrackedApp("com.zaneschepke.wireguardautotunnel", "WG Tunnel"),
|
||||
TrackedApp("moe.nb4a", "NekoBox"),
|
||||
TrackedApp("fr.husi", "husi"),
|
||||
TrackedApp("com.outline.android", "Outline"),
|
||||
TrackedApp("xyz.safetyvpn.app", "SafetyVPN"),
|
||||
TrackedApp("net.mullvad.mullvadvpn", "Mullvad VPN"),
|
||||
TrackedApp("org.torproject.android", "Orbot")
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue