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