package com.frostnerd.smokescreen.fragment
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.net.Uri
import android.net.VpnService
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import com.frostnerd.dnstunnelproxy.DnsServerInformation
import com.frostnerd.encrypteddnstunnelproxy.AbstractHttpsDNSHandle
import com.frostnerd.encrypteddnstunnelproxy.HttpsDnsServerInformation
import com.frostnerd.encrypteddnstunnelproxy.quic.QuicUpstreamAddress
import com.frostnerd.encrypteddnstunnelproxy.tls.AbstractTLSDnsHandle
import com.frostnerd.general.service.isServiceRunning
import com.frostnerd.lifecyclemanagement.launchWithLifecycle
import com.frostnerd.lifecyclemanagement.launchWithLifecycleUi
import com.frostnerd.smokescreen.*
import com.frostnerd.smokescreen.activity.PinActivity
import com.frostnerd.smokescreen.activity.SpeedTestActivity
import com.frostnerd.smokescreen.dialog.ServerChoosalDialog
import com.frostnerd.smokescreen.service.Command
import com.frostnerd.smokescreen.service.DnsVpnService
import com.frostnerd.smokescreen.util.ServerType
import com.frostnerd.smokescreen.util.speedtest.DnsSpeedTest
import kotlinx.android.synthetic.main.fragment_main.*
import kotlinx.coroutines.*
import java.net.URL
/*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class MainFragment : Fragment() {
private val vpnRequestCode: Int = 1
private var proxyState:ProxyState = ProxyState.NOT_RUNNING
private var vpnStateReceiver: BroadcastReceiver? = null
private var latencyCheckJob:Job? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_main, container, false)
}
override fun onResume() {
super.onResume()
proxyState = if(requireContext().isServiceRunning(DnsVpnService::class.java)) {
if(DnsVpnService.paused) ProxyState.PAUSED
else ProxyState.RUNNING
} else ProxyState.NOT_RUNNING
updateVpnIndicators()
context?.clearPreviousIptablesRedirect()
runLatencyCheck()
determineLatencyBounds()
displayServer(getPreferences().dnsServerConfig)
GlobalScope.launch {
val context = context
if (isAdded && !isDetached && context != null) {
updatePrivacyPolicyLink(getPreferences().dnsServerConfig)
}
}
}
override fun onStart() {
super.onStart()
vpnStateReceiver = requireContext().registerLocalReceiver(
listOf(
DnsVpnService.BROADCAST_VPN_ACTIVE,
DnsVpnService.BROADCAST_VPN_INACTIVE,
DnsVpnService.BROADCAST_VPN_PAUSED,
DnsVpnService.BROADCAST_VPN_RESUMED
)
) {
if (it != null && it.action != null && isAdded) {
when (it.action) {
DnsVpnService.BROADCAST_VPN_ACTIVE, DnsVpnService.BROADCAST_VPN_RESUMED -> {
proxyState = ProxyState.RUNNING
}
DnsVpnService.BROADCAST_VPN_INACTIVE -> {
proxyState = ProxyState.NOT_RUNNING
displayServer(getPreferences().dnsServerConfig)
}
DnsVpnService.BROADCAST_VPN_PAUSED -> {
proxyState = ProxyState.PAUSED
}
}
updateVpnIndicators()
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
startButton.setOnClickListener {
proxyState = when (proxyState) {
ProxyState.RUNNING -> {
DnsVpnService.sendCommand(
requireContext(),
Command.STOP,
PinActivity.passPinExtras()
)
ProxyState.NOT_RUNNING
}
ProxyState.PAUSED -> {
DnsVpnService.sendCommand(it.context, Command.PAUSE_RESUME)
ProxyState.STARTING
}
else -> {
startVpn()
ProxyState.STARTING
}
}
updateVpnIndicators()
}
startButton.setOnTouchListener { innerView, event ->
if (proxyState == ProxyState.RUNNING || proxyState == ProxyState.STARTING) {
false
} else {
var handled = false
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0) {
if (event.action == MotionEvent.ACTION_UP) {
if (!getPreferences().runWithoutVpn && VpnService.prepare(requireContext()) != null) {
showInfoTextDialog(requireContext(),
getString(R.string.dialog_overlaydetected_title),
getString(R.string.dialog_overlaydetected_message),
positiveButton = null,
negativeButton = null,
neutralButton = getString(android.R.string.ok) to { dialog, _ ->
dialog.dismiss()
startVpn()
proxyState = ProxyState.STARTING
}
)
handled = true
}
}
}
if(event.action == MotionEvent.ACTION_UP && !handled) {
innerView.performClick()
}
true
}
}
speedTest.setOnClickListener {
startActivity(Intent(requireContext(), SpeedTestActivity::class.java))
}
mainServerWrap.setOnClickListener {
ServerChoosalDialog(requireActivity() as AppCompatActivity) { config ->
updatePrivacyPolicyLink(config)
val prefs = requireContext().getPreferences()
prefs.edit {
prefs.dnsServerConfig = config
}
displayServer(config)
}.show()
}
privacyStatementText.setOnClickListener {
if (it.tag != null) {
val i = Intent(Intent.ACTION_VIEW)
val url = it.tag as URL
i.data = Uri.parse(url.toURI().toString())
try {
startActivity(i)
} catch (e: ActivityNotFoundException) {
Toast.makeText(
requireContext(),
R.string.error_no_webbrowser_installed,
Toast.LENGTH_LONG
).show()
}
}
}
}
private fun displayServer(config: DnsServerInformation<*>) {
serverName.text = config.name
serverURL.text = when(config.type) {
ServerType.DOH -> (config as HttpsDnsServerInformation).servers.firstOrNull()?.address?.getUrl(
true
) ?: "-"
ServerType.DOT -> config.servers.firstOrNull()?.address?.formatToString() ?: "-"
ServerType.DOQ -> (config.servers.firstOrNull()?.address as? QuicUpstreamAddress)?.getUrl(
true
) ?: "-"
}
serverLatency.text = "-\nms"
serverIndicator.backgroundTintList = null
mainServerWrap.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
serverIndicator.updateLayoutParams {
height = mainServerWrap.measuredHeight
}
}
}
override fun onStop() {
super.onStop()
vpnStateReceiver?.also { requireContext().unregisterLocalReceiver(it) }
}
private fun startVpn() {
if(getPreferences().runWithoutVpn) {
requireContext().startService(Intent(requireContext(), DnsVpnService::class.java))
} else {
val prepare = VpnService.prepare(requireContext()).apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
}
if (prepare == null) {
requireContext().startService(Intent(requireContext(), DnsVpnService::class.java))
getPreferences().vpnInformationShown = true
} else {
if (getPreferences().vpnInformationShown) {
startActivityForResult(prepare, vpnRequestCode)
} else {
showInfoTextDialog(requireContext(),
getString(R.string.dialog_vpninformation_title),
getString(R.string.dialog_vpninformation_message),
neutralButton = getString(android.R.string.ok) to { dialog, _ ->
startActivityForResult(prepare, vpnRequestCode)
dialog.dismiss()
}, withDialog = {
setCancelable(false)
})
getPreferences().vpnInformationShown = true
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == vpnRequestCode && resultCode == Activity.RESULT_OK) {
startVpn()
} else if (requestCode == vpnRequestCode) {
updateVpnIndicators()
}
}
private fun updateVpnIndicators() {
val privateDnsActive = context?.isPrivateDnsActive ?: return
var startButtonEnabled = true
var privacyTextVisibility = View.VISIBLE
var privateDNSVisibility = View.GONE
var statusTxt:Int = R.string.window_main_unprotected
var enableInfoVisibility = View.VISIBLE
when(proxyState) {
ProxyState.RUNNING -> {
startButton.setImageResource(R.drawable.ic_lock)
statusTxt = R.string.window_main_protected
enableInfoVisibility = View.INVISIBLE
}
ProxyState.STARTING -> {
startButton.setImageResource(R.drawable.ic_lock_half_open)
enableInfoVisibility = View.INVISIBLE
}
ProxyState.PAUSED -> {
startButton.setImageResource(R.drawable.ic_lock_half_open)
statusTxt = R.string.window_main_unprotected
serverLatency.text = "-\nms"
}
else -> {
serverLatency.text = "-\nms"
if (privateDnsActive) {
startButton.setImageResource(R.drawable.ic_lock)
privacyTextVisibility = View.GONE
startButtonEnabled = false
privateDNSVisibility = View.VISIBLE
statusTxt = R.string.window_main_protected
enableInfoVisibility = View.INVISIBLE
} else {
startButton.setImageResource(R.drawable.ic_lock_open)
privateDnsInfo.visibility = View.INVISIBLE
statusTxt = R.string.window_main_unprotected
}
}
}
startButton.isEnabled = startButtonEnabled
privateDnsInfo.visibility = privateDNSVisibility
privacyTextWrap.visibility = privacyTextVisibility
enableInformation.visibility = enableInfoVisibility
statusText.setText(statusTxt)
}
private fun updatePrivacyPolicyLink(serverInfo: DnsServerInformation<*>) {
activity?.let { activity ->
if (!serverInfo.specification.privacyPolicyURL.isNullOrBlank()) {
launchWithLifecycle {
val url = URL(serverInfo.specification.privacyPolicyURL)
launchUi {
val text = view?.findViewById(R.id.privacyStatementText)
text?.text =
getString(
R.string.main_dnssurveillance_privacystatement,
serverInfo.name
)
text?.tag = url
text?.visibility = View.VISIBLE
}
}
} else {
launchWithLifecycleUi {
val text = view?.findViewById(R.id.privacyStatementText)
text?.visibility = View.GONE
}
}
}
}
private var greatLatencyThreshold = 130
private var goodLatencyThreshold = 200
private var averageLatencyThreshold = 310
private fun runLatencyCheck() {
latencyCheckJob = launchWithLifecycle(cancelOn = setOf(Lifecycle.Event.ON_PAUSE)) {
if(isActive) {
launchUi {
if(view != null) {
val latency = DnsVpnService.currentTrafficStats?.floatingAverageLatency?.takeIf { it > 0 }
if(latency != null) {
serverLatency?.visibility = View.VISIBLE
serverLatency?.text = latency.let { "$it\nms" }
val color = when {
latency < greatLatencyThreshold -> Color.parseColor("#43A047")
latency < goodLatencyThreshold -> Color.parseColor("#9CCC65")
latency < averageLatencyThreshold -> Color.parseColor("#FFB300")
else -> Color.parseColor("#E53935")
}
serverIndicator.backgroundTintList = ColorStateList.valueOf(color)
delay(750)
} else {
serverLatency?.visibility = View.INVISIBLE
serverLatency?.text = "-\nms"
serverIndicator?.backgroundTintList = null
delay(1500)
}
}
runLatencyCheck()
}
}
}
}
private fun determineLatencyBounds() {
val context = requireContext()
// Use the ping for the best servers to deviate the thresholds
// The deviation will only increase the thresholds, not decrease it.
// This is to avoid measuring smaller servers on the benchmarks of the "better" ones
// But if a "good" server has a bad connection, chances are the smaller ones do as well
// And in that case the smaller server should be measured on the bigger ones to have a point of reference
// as the values I chose are between average to best-case, not worst-case.
launchWithLifecycle {
val fastServerAverage = (AbstractHttpsDNSHandle.suspendUntilKnownServersArePopulated(
1500
) {
setOf(it[0], it[1], it[3]) // Google, CF, Quad9
} + AbstractTLSDnsHandle.suspendUntilKnownServersArePopulated(1500) {
setOf(it[1], it[0]) //Quad9, CF
}).mapNotNull {
DnsSpeedTest(context, it as DnsServerInformation<*>, log = {}, cronetEngine = null /* We do not need quic here*/ ).runTest(4)
}.takeIf {
it.isNotEmpty()
}?.let {
it.sum() / it.size
} ?: return@launchWithLifecycle
val rawFactor = maxOf(
greatLatencyThreshold.toDouble(),
greatLatencyThreshold * (fastServerAverage.toDouble() / greatLatencyThreshold)
)/greatLatencyThreshold
val adjustmentFactor = 1 + (rawFactor - 1)/2
val pingStepAdjustment = (12*rawFactor)-12 //High deviation from 100ms -> Higher differences between steps in rating
greatLatencyThreshold = (greatLatencyThreshold * adjustmentFactor + pingStepAdjustment*0.8).toInt()
goodLatencyThreshold = (goodLatencyThreshold * adjustmentFactor + pingStepAdjustment*1.2).toInt()
averageLatencyThreshold = (goodLatencyThreshold * adjustmentFactor + pingStepAdjustment*1.7).toInt()
}
}
enum class ProxyState {
NOT_RUNNING, STARTING, RUNNING, PAUSED
}
}