убрано фоновое отслеживание, приложение переведено на manual refresh

This commit is contained in:
p.frasyn 2026-04-09 17:05:41 +03:00
parent b036ee8400
commit 356bca8d26
5 changed files with 13 additions and 285 deletions

View file

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

View file

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

View file

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

View file

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