MainFragment.kt 17.8 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
66
    private var currentDisplayedServerHash:Int? = null
Daniel Wolf's avatar
Daniel Wolf committed
67

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

    override fun onResume() {
        super.onResume()
78
        val previousProxyState = proxyState
79
80
81
82
        proxyState = if(requireContext().isServiceRunning(DnsVpnService::class.java)) {
            if(DnsVpnService.paused) ProxyState.PAUSED
            else ProxyState.RUNNING
        } else ProxyState.NOT_RUNNING
83
84
        if(proxyState != previousProxyState) {
            updateVpnIndicators()
85
        }
86
87
88
89
        if(proxyState != previousProxyState || currentDisplayedServerHash == null) {
            displayServer(getPreferences().dnsServerConfig)
        }
        runLatencyCheck()
90
91
92
93
    }

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

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

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

227
228
229
    override fun onStop() {
        super.onStop()
        vpnStateReceiver?.also {  requireContext().unregisterLocalReceiver(it)  }
Daniel Wolf's avatar
Daniel Wolf committed
230
231
    }

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

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

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
263
        if (requestCode == vpnRequestCode && resultCode == Activity.RESULT_OK) {
Daniel Wolf's avatar
Daniel Wolf committed
264
            startVpn()
265
        } else if (requestCode == vpnRequestCode) {
266
            updateVpnIndicators()
Daniel Wolf's avatar
Daniel Wolf committed
267
268
269
        }
    }

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

315
    private fun updatePrivacyPolicyLink(serverInfo: DnsServerInformation<*>) {
Daniel Wolf's avatar
Daniel Wolf committed
316
        activity?.let { _ ->
317
            if (isAdded && !isDetached && !serverInfo.specification.privacyPolicyURL.isNullOrBlank()) {
Daniel Wolf's avatar
Daniel Wolf committed
318
                launchWithLifecycle {
Daniel Wolf's avatar
Daniel Wolf committed
319
                    val url = URL(serverInfo.specification.privacyPolicyURL)
Daniel Wolf's avatar
Daniel Wolf committed
320
                    launchUi {
Daniel Wolf's avatar
Daniel Wolf committed
321
322
323
324
325
326
327
328
329
330
331
                        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
332
                launchWithLifecycleUi {
Daniel Wolf's avatar
Daniel Wolf committed
333
334
335
                    val text = view?.findViewById<TextView>(R.id.privacyStatementText)
                    text?.visibility = View.GONE
                }
336
            }
337
338
        }
    }
339

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

374
    private fun determineLatencyBounds() {
Daniel Wolf's avatar
Daniel Wolf committed
375
        val context = requireContext()
376
377
378
379
380
381
        // 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.
382
383
        if(!context.getPreferences().compareDnsSpeedsAtLaunch) return

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

419
420
421
    enum class ProxyState {
        NOT_RUNNING, STARTING, RUNNING, PAUSED
    }
Daniel Wolf's avatar
Daniel Wolf committed
422
}