MainFragment.kt 17.5 KB
Newer Older
Daniel Wolf's avatar
Daniel Wolf committed
1
2
3
package com.frostnerd.smokescreen.fragment

import android.app.Activity
4
import android.content.ActivityNotFoundException
Daniel Wolf's avatar
Daniel Wolf committed
5
6
import android.content.BroadcastReceiver
import android.content.Intent
7
8
import android.content.res.ColorStateList
import android.graphics.Color
9
import android.net.Uri
Daniel Wolf's avatar
Daniel Wolf committed
10
11
12
import android.net.VpnService
import android.os.Bundle
import android.view.LayoutInflater
13
import android.view.MotionEvent
Daniel Wolf's avatar
Daniel Wolf committed
14
15
import android.view.View
import android.view.ViewGroup
16
import android.widget.TextView
17
import android.widget.Toast
Daniel Wolf's avatar
Daniel Wolf committed
18
import androidx.appcompat.app.AppCompatActivity
19
import androidx.core.view.updateLayoutParams
Daniel Wolf's avatar
Daniel Wolf committed
20
import androidx.fragment.app.Fragment
Daniel Wolf's avatar
Daniel Wolf committed
21
import androidx.lifecycle.Lifecycle
Daniel Wolf's avatar
Daniel Wolf committed
22
import com.frostnerd.dnstunnelproxy.DnsServerInformation
23
import com.frostnerd.encrypteddnstunnelproxy.AbstractHttpsDNSHandle
Daniel Wolf's avatar
Daniel Wolf committed
24
import com.frostnerd.encrypteddnstunnelproxy.HttpsDnsServerInformation
Daniel Wolf's avatar
Daniel Wolf committed
25
import com.frostnerd.encrypteddnstunnelproxy.quic.QuicUpstreamAddress
26
import com.frostnerd.encrypteddnstunnelproxy.tls.AbstractTLSDnsHandle
Daniel Wolf's avatar
Daniel Wolf committed
27
import com.frostnerd.general.service.isServiceRunning
Daniel Wolf's avatar
Daniel Wolf committed
28
import com.frostnerd.lifecyclemanagement.launchWithLifecycle
29
import com.frostnerd.lifecyclemanagement.launchWithLifecycleUi
30
import com.frostnerd.smokescreen.*
31
import com.frostnerd.smokescreen.activity.PinActivity
32
import com.frostnerd.smokescreen.activity.SpeedTestActivity
33
import com.frostnerd.smokescreen.dialog.ServerChoosalDialog
Daniel Wolf's avatar
Daniel Wolf committed
34
35
import com.frostnerd.smokescreen.service.Command
import com.frostnerd.smokescreen.service.DnsVpnService
Daniel Wolf's avatar
Daniel Wolf committed
36
import com.frostnerd.smokescreen.util.ServerType
37
import com.frostnerd.smokescreen.util.speedtest.DnsSpeedTest
Daniel Wolf's avatar
Daniel Wolf committed
38
import kotlinx.android.synthetic.main.fragment_main.*
Daniel Wolf's avatar
Daniel Wolf committed
39
import kotlinx.coroutines.*
Daniel Wolf's avatar
Daniel Wolf committed
40
import java.net.URL
Daniel Wolf's avatar
Daniel Wolf committed
41
42


Daniel Wolf's avatar
Daniel Wolf committed
43
44
/*
 * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
Daniel Wolf's avatar
Daniel Wolf committed
45
 *
Daniel Wolf's avatar
Daniel Wolf committed
46
47
48
49
50
51
52
53
54
55
56
57
58
59
 * 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 <http://www.gnu.org/licenses/>.
 *
 * You can contact the developer at daniel.wolf@frostnerd.com.
Daniel Wolf's avatar
Daniel Wolf committed
60
 */
61
62
class MainFragment : Fragment() {
    private val vpnRequestCode: Int = 1
63
    private var proxyState:ProxyState = ProxyState.NOT_RUNNING
64
    private var vpnStateReceiver: BroadcastReceiver? = null
Daniel Wolf's avatar
Daniel Wolf committed
65
    private var latencyCheckJob:Job? = null
Daniel Wolf's avatar
Daniel Wolf committed
66

67
68
69
70
71
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
Daniel Wolf's avatar
Daniel Wolf committed
72
73
74
75
76
        return inflater.inflate(R.layout.fragment_main, container, false)
    }

    override fun onResume() {
        super.onResume()
77
        val previousProxyState = proxyState
78
79
80
81
        proxyState = if(requireContext().isServiceRunning(DnsVpnService::class.java)) {
            if(DnsVpnService.paused) ProxyState.PAUSED
            else ProxyState.RUNNING
        } else ProxyState.NOT_RUNNING
82
83
        if(proxyState != previousProxyState) {
            updateVpnIndicators()
84
        }
Daniel Wolf's avatar
Daniel Wolf committed
85
        displayServer(getPreferences().dnsServerConfig)
86
        runLatencyCheck()
87
88
89
90
    }

    override fun onStart() {
        super.onStart()
91
92
        context?.clearPreviousIptablesRedirect()
        determineLatencyBounds()
93
94
95
96
97
98
99
100
        vpnStateReceiver = requireContext().registerLocalReceiver(
            listOf(
                DnsVpnService.BROADCAST_VPN_ACTIVE,
                DnsVpnService.BROADCAST_VPN_INACTIVE,
                DnsVpnService.BROADCAST_VPN_PAUSED,
                DnsVpnService.BROADCAST_VPN_RESUMED
            )
        ) {
101
            if (it != null && it.action != null && isAdded) {
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
                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()
            }
        }
Daniel Wolf's avatar
Daniel Wolf committed
117
118
119
120
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        startButton.setOnClickListener {
121
122
            proxyState = when (proxyState) {
                ProxyState.RUNNING -> {
123
124
125
126
127
                    DnsVpnService.sendCommand(
                        requireContext(),
                        Command.STOP,
                        PinActivity.passPinExtras()
                    )
128
129
130
131
132
133
134
135
136
137
                    ProxyState.NOT_RUNNING
                }
                ProxyState.PAUSED -> {
                    DnsVpnService.sendCommand(it.context, Command.PAUSE_RESUME)
                    ProxyState.STARTING
                }
                else -> {
                    startVpn()
                    ProxyState.STARTING
                }
Daniel Wolf's avatar
Daniel Wolf committed
138
            }
139
            updateVpnIndicators()
Daniel Wolf's avatar
Daniel Wolf committed
140
        }
Daniel Wolf's avatar
Daniel Wolf committed
141
        startButton.setOnTouchListener { innerView, event ->
142
            if (proxyState == ProxyState.RUNNING || proxyState == ProxyState.STARTING) {
143
144
                false
            } else {
145
                var handled = false
146
147
                if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0) {
                    if (event.action == MotionEvent.ACTION_UP) {
148
                        if (!getPreferences().runWithoutVpn && VpnService.prepare(requireContext()) != null) {
149
                            showInfoTextDialog(requireContext(),
150
151
152
153
154
155
156
                                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()
157
                                    proxyState = ProxyState.STARTING
158
159
                                }
                            )
160
161
162
163
                            handled = true
                        }
                    }
                }
164
                if(event.action == MotionEvent.ACTION_UP && !handled) {
165
166
167
                    innerView.performClick()
                }
                true
168
169
            }
        }
170
        speedTest.setOnClickListener {
171
            startActivity(Intent(requireContext(), SpeedTestActivity::class.java))
172
        }
Daniel Wolf's avatar
Daniel Wolf committed
173
        mainServerWrap.setOnClickListener {
174
            ServerChoosalDialog(requireActivity() as AppCompatActivity) { config ->
Daniel Wolf's avatar
Daniel Wolf committed
175
                updatePrivacyPolicyLink(config)
176
                val prefs = requireContext().getPreferences()
177
                prefs.edit {
Daniel Wolf's avatar
Daniel Wolf committed
178
                    prefs.dnsServerConfig = config
179
                }
Daniel Wolf's avatar
Daniel Wolf committed
180
                displayServer(config)
181
            }.show()
Daniel Wolf's avatar
Daniel Wolf committed
182
        }
183
        privacyStatementText.setOnClickListener {
184
            if (it.tag != null) {
185
186
187
                val i = Intent(Intent.ACTION_VIEW)
                val url = it.tag as URL
                i.data = Uri.parse(url.toURI().toString())
188
189
                try {
                    startActivity(i)
190
191
                } catch (e: ActivityNotFoundException) {
                    Toast.makeText(
192
                        requireContext(),
193
194
195
196
                        R.string.error_no_webbrowser_installed,
                        Toast.LENGTH_LONG
                    ).show()
                }
197
            }
198
        }
199
200
201
202
203
        mainServerWrap.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
            serverIndicator.updateLayoutParams {
                height = mainServerWrap.measuredHeight
            }
        }
Daniel Wolf's avatar
Daniel Wolf committed
204
205
    }

Daniel Wolf's avatar
Daniel Wolf committed
206
    private fun displayServer(config: DnsServerInformation<*>) {
Daniel Wolf's avatar
Daniel Wolf committed
207
        serverName.text = config.name
Daniel Wolf's avatar
Daniel Wolf committed
208
        serverURL.text = when(config.type) {
Daniel Wolf's avatar
Daniel Wolf committed
209
210
211
            ServerType.DOH -> (config as HttpsDnsServerInformation).servers.firstOrNull()?.address?.getUrl(
                true
            ) ?: "-"
Daniel Wolf's avatar
Daniel Wolf committed
212
            ServerType.DOT -> config.servers.firstOrNull()?.address?.formatToString() ?: "-"
Daniel Wolf's avatar
Daniel Wolf committed
213
214
215
            ServerType.DOQ -> (config.servers.firstOrNull()?.address as? QuicUpstreamAddress)?.getUrl(
                true
            ) ?: "-"
Daniel Wolf's avatar
Daniel Wolf committed
216
        }
217
        serverLatency.text = "-\nms"
218
        serverIndicator.backgroundTintList = null
219
        updatePrivacyPolicyLink(config)
Daniel Wolf's avatar
Daniel Wolf committed
220
221
    }

222
223
224
    override fun onStop() {
        super.onStop()
        vpnStateReceiver?.also {  requireContext().unregisterLocalReceiver(it)  }
Daniel Wolf's avatar
Daniel Wolf committed
225
226
    }

Daniel Wolf's avatar
Daniel Wolf committed
227
    private fun startVpn() {
228
        if(getPreferences().runWithoutVpn) {
Daniel Wolf's avatar
Daniel Wolf committed
229
230
            requireContext().startService(Intent(requireContext(), DnsVpnService::class.java))
        } else {
231
232
233
234
235
236
            val prepare = VpnService.prepare(requireContext()).apply {
                this?.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
            }

            if (prepare == null) {
                requireContext().startService(Intent(requireContext(), DnsVpnService::class.java))
237
                getPreferences().vpnInformationShown = true
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
            } 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
                }
253
            }
Daniel Wolf's avatar
Daniel Wolf committed
254
255
256
257
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
258
        if (requestCode == vpnRequestCode && resultCode == Activity.RESULT_OK) {
Daniel Wolf's avatar
Daniel Wolf committed
259
            startVpn()
260
        } else if (requestCode == vpnRequestCode) {
261
            updateVpnIndicators()
Daniel Wolf's avatar
Daniel Wolf committed
262
263
264
        }
    }

265
    private fun updateVpnIndicators() {
266
        val privateDnsActive = context?.isPrivateDnsActive ?: return
Daniel Wolf's avatar
Daniel Wolf committed
267
        var startButtonEnabled = true
268
        var privacyTextVisibility = View.VISIBLE
Daniel Wolf's avatar
Daniel Wolf committed
269
        var privateDNSVisibility = View.GONE
Daniel Wolf's avatar
Daniel Wolf committed
270
271
        var statusTxt:Int = R.string.window_main_unprotected
        var enableInfoVisibility = View.VISIBLE
272
273
        when(proxyState) {
            ProxyState.RUNNING -> {
Daniel Wolf's avatar
Daniel Wolf committed
274
                startButton.setImageResource(R.drawable.ic_lock)
Daniel Wolf's avatar
Daniel Wolf committed
275
                statusTxt = R.string.window_main_protected
276
                enableInfoVisibility = View.INVISIBLE
Daniel Wolf's avatar
Daniel Wolf committed
277
            }
278
            ProxyState.STARTING -> {
Daniel Wolf's avatar
Daniel Wolf committed
279
                startButton.setImageResource(R.drawable.ic_lock_half_open)
280
                enableInfoVisibility = View.INVISIBLE
Daniel Wolf's avatar
Daniel Wolf committed
281
            }
282
            ProxyState.PAUSED -> {
Daniel Wolf's avatar
Daniel Wolf committed
283
                startButton.setImageResource(R.drawable.ic_lock_half_open)
Daniel Wolf's avatar
Daniel Wolf committed
284
                statusTxt = R.string.window_main_unprotected
285
                serverLatency.text = "-\nms"
286
            }
Daniel Wolf's avatar
Daniel Wolf committed
287
            else -> {
288
                serverLatency.text = "-\nms"
289
                if (privateDnsActive) {
Daniel Wolf's avatar
Daniel Wolf committed
290
291
292
293
                    startButton.setImageResource(R.drawable.ic_lock)
                    privacyTextVisibility = View.GONE
                    startButtonEnabled = false
                    privateDNSVisibility = View.VISIBLE
Daniel Wolf's avatar
Daniel Wolf committed
294
                    statusTxt = R.string.window_main_protected
295
                    enableInfoVisibility = View.INVISIBLE
296
                } else {
Daniel Wolf's avatar
Daniel Wolf committed
297
                    startButton.setImageResource(R.drawable.ic_lock_open)
298
                    privateDnsInfo.visibility = View.INVISIBLE
Daniel Wolf's avatar
Daniel Wolf committed
299
                    statusTxt = R.string.window_main_unprotected
300
                }
Daniel Wolf's avatar
Daniel Wolf committed
301
302
            }
        }
Daniel Wolf's avatar
Daniel Wolf committed
303
304
        startButton.isEnabled = startButtonEnabled
        privateDnsInfo.visibility = privateDNSVisibility
305
        privacyTextWrap.visibility = privacyTextVisibility
Daniel Wolf's avatar
Daniel Wolf committed
306
307
        enableInformation.visibility = enableInfoVisibility
        statusText.setText(statusTxt)
Daniel Wolf's avatar
Daniel Wolf committed
308
    }
309

310
    private fun updatePrivacyPolicyLink(serverInfo: DnsServerInformation<*>) {
Daniel Wolf's avatar
Daniel Wolf committed
311
        activity?.let { _ ->
312
            if (isAdded && !isDetached && !serverInfo.specification.privacyPolicyURL.isNullOrBlank()) {
Daniel Wolf's avatar
Daniel Wolf committed
313
                launchWithLifecycle {
Daniel Wolf's avatar
Daniel Wolf committed
314
                    val url = URL(serverInfo.specification.privacyPolicyURL)
Daniel Wolf's avatar
Daniel Wolf committed
315
                    launchUi {
Daniel Wolf's avatar
Daniel Wolf committed
316
317
318
319
320
321
322
323
324
325
326
                        val text = view?.findViewById<TextView>(R.id.privacyStatementText)
                        text?.text =
                            getString(
                                R.string.main_dnssurveillance_privacystatement,
                                serverInfo.name
                            )
                        text?.tag = url
                        text?.visibility = View.VISIBLE
                    }
                }
            } else {
Daniel Wolf's avatar
Daniel Wolf committed
327
                launchWithLifecycleUi {
Daniel Wolf's avatar
Daniel Wolf committed
328
329
330
                    val text = view?.findViewById<TextView>(R.id.privacyStatementText)
                    text?.visibility = View.GONE
                }
331
            }
332
333
        }
    }
334

Daniel Wolf's avatar
Daniel Wolf committed
335
336
337
    private var greatLatencyThreshold = 130
    private var goodLatencyThreshold = 200
    private  var averageLatencyThreshold = 310
338
    private fun runLatencyCheck() {
339
        latencyCheckJob?.cancel()
Daniel Wolf's avatar
Daniel Wolf committed
340
341
342
        latencyCheckJob = launchWithLifecycle(cancelOn = setOf(Lifecycle.Event.ON_PAUSE)) {
            if(isActive) {
                launchUi {
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
                    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)
361
362
                        }
                    }
Daniel Wolf's avatar
Daniel Wolf committed
363
364
365
366
367
368
                    runLatencyCheck()
                }
            }
        }
    }

369
    private fun determineLatencyBounds() {
Daniel Wolf's avatar
Daniel Wolf committed
370
        val context = requireContext()
371
372
373
374
375
376
        // 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.
377
378
        if(!context.getPreferences().compareDnsSpeedsAtLaunch) return

379
        launchWithLifecycle {
380
            val httpsEngine =  createHttpCronetEngineIfInstalled(context)
Daniel Wolf's avatar
Daniel Wolf committed
381
382
383
            val fastServerAverage = (AbstractHttpsDNSHandle.suspendUntilKnownServersArePopulated(
                1500
            ) {
384
385
386
                setOf(it[0], it[1], it[3]) // Google, CF, Quad9
            } + AbstractTLSDnsHandle.suspendUntilKnownServersArePopulated(1500) {
                setOf(it[1], it[0]) //Quad9, CF
Daniel Wolf's avatar
Daniel Wolf committed
387
            }).filterNotNull().mapNotNull {
388
389
390
391
392
393
394
                DnsSpeedTest(
                    context,
                    it as DnsServerInformation<*>,
                    log = {},
                    cronetEngine = null, /* We do not need quic here*/
                    httpsCronetEngine = httpsEngine
                ).runTest(4)
395
396
397
            }.takeIf {
                it.isNotEmpty()
            }?.let {
398
                it.sum() / it.size
399
400
            }.also {
                httpsEngine?.shutdown()
401
            } ?: return@launchWithLifecycle
Daniel Wolf's avatar
Daniel Wolf committed
402
403
404
405
            val rawFactor = maxOf(
                greatLatencyThreshold.toDouble(),
                greatLatencyThreshold * (fastServerAverage.toDouble() / greatLatencyThreshold)
            )/greatLatencyThreshold
406
            val adjustmentFactor = 1 + (rawFactor - 1)/2
407
            val pingStepAdjustment = (12*rawFactor)-12 //High deviation from 100ms -> Higher differences between steps in rating
408
409
410
411
412
413
            greatLatencyThreshold = (greatLatencyThreshold * adjustmentFactor + pingStepAdjustment*0.8).toInt()
            goodLatencyThreshold = (goodLatencyThreshold * adjustmentFactor + pingStepAdjustment*1.2).toInt()
            averageLatencyThreshold = (goodLatencyThreshold * adjustmentFactor + pingStepAdjustment*1.7).toInt()
        }
    }

414
415
416
    enum class ProxyState {
        NOT_RUNNING, STARTING, RUNNING, PAUSED
    }
Daniel Wolf's avatar
Daniel Wolf committed
417
}