diff --git a/tracked_apps.json b/app/src/main/assets/tracked_apps.json
similarity index 100%
rename from tracked_apps.json
rename to app/src/main/assets/tracked_apps.json
diff --git a/app/src/main/java/com/cherepavel/vpndetector/MainActivity.kt b/app/src/main/java/com/cherepavel/vpndetector/MainActivity.kt
index 5334839..3cc2090 100644
--- a/app/src/main/java/com/cherepavel/vpndetector/MainActivity.kt
+++ b/app/src/main/java/com/cherepavel/vpndetector/MainActivity.kt
@@ -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)
diff --git a/app/src/main/java/com/cherepavel/vpndetector/service/VpnMonitorService.kt b/app/src/main/java/com/cherepavel/vpndetector/service/VpnMonitorService.kt
deleted file mode 100644
index b2fdea1..0000000
--- a/app/src/main/java/com/cherepavel/vpndetector/service/VpnMonitorService.kt
+++ /dev/null
@@ -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))
- }
- }
-}
diff --git a/detector/src/main/AndroidManifest.xml b/detector/src/main/AndroidManifest.xml
index cbb96c1..b2d3ea1 100644
--- a/detector/src/main/AndroidManifest.xml
+++ b/detector/src/main/AndroidManifest.xml
@@ -1,6 +1,2 @@
-
-
-
-
-
\ No newline at end of file
+
diff --git a/detector/src/main/java/com/cherepavel/vpndetector/detector/TrackedAppsRepository.kt b/detector/src/main/java/com/cherepavel/vpndetector/detector/TrackedAppsRepository.kt
index fbe1528..7055c47 100644
--- a/detector/src/main/java/com/cherepavel/vpndetector/detector/TrackedAppsRepository.kt
+++ b/detector/src/main/java/com/cherepavel/vpndetector/detector/TrackedAppsRepository.kt
@@ -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? = null
- @Volatile private var cached: List? = 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 {
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")
- )
}
+