Commit 554f8fcf authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Added logic to the speed test activity. Rows can be clicked and show the name,...

Added logic to the speed test activity. Rows can be clicked and show the name, server address and latency. Clicking a row asks the user whether he wants to use it.
parent 512eff4b
......@@ -170,7 +170,11 @@
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING"/>
</intent-filter>
</activity>
<activity android:name=".activity.SpeedTestActivity"/>
<activity
android:name=".activity.SpeedTestActivity"
android:theme="@style/AppTheme_Mono"
android:label="@string/activity_label_speed_test"
/>
<service
android:name=".service.DnsVpnService"
......
package com.frostnerd.smokescreen.activity
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.frostnerd.cacheadapter.AdapterBuilder
import com.frostnerd.cacheadapter.DataSource
import com.frostnerd.cacheadapter.ListDataSource
import com.frostnerd.dnstunnelproxy.DnsServerInformation
import com.frostnerd.encrypteddnstunnelproxy.AbstractHttpsDNSHandle
import com.frostnerd.encrypteddnstunnelproxy.tls.AbstractTLSDnsHandle
import com.frostnerd.lifecyclemanagement.BaseActivity
import com.frostnerd.lifecyclemanagement.BaseViewHolder
import com.frostnerd.lifecyclemanagement.LifecycleCoroutineScope
import com.frostnerd.lifecyclemanagement.launchWithLifecylce
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.getPreferences
import com.frostnerd.smokescreen.util.speedtest.DnsSpeedTest
import kotlinx.android.synthetic.main.activity_speedtest.*
import kotlinx.android.synthetic.main.fragment_main.*
import kotlinx.android.synthetic.main.item_dns_speed.view.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import android.R.attr.bottom
import android.graphics.Rect
import androidx.lifecycle.Lifecycle
import com.frostnerd.design.DesignUtil
import com.frostnerd.encrypteddnstunnelproxy.HttpsDnsServerInformation
import com.frostnerd.smokescreen.showInfoTextDialog
/*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r)
......@@ -21,9 +53,213 @@ import com.frostnerd.lifecyclemanagement.BaseActivity
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class SpeedTestActivity :BaseActivity() {
class SpeedTestActivity : BaseActivity() {
private var testRunning = false
private var testJob: Job? = null
private var testResults:List<SpeedTest>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_speedtest)
setSupportActionBar(toolBar)
startTest.setOnClickListener {
startTest()
testResults = null
startTest.isEnabled = false
abort.visibility = View.VISIBLE
info.visibility = View.GONE
}
abort.setOnClickListener {
abort.visibility = View.GONE
testJob?.cancel()
startTest.isEnabled = true
testRunning = false
info.visibility = View.VISIBLE
startTest.text = getString(R.string.window_speedtest_runtest)
}
info.setOnClickListener {
if(testResults != null) {
val dotCount = testResults!!.count { it.server !is HttpsDnsServerInformation }
val dotReachable = testResults!!.count { it.server !is HttpsDnsServerInformation && it.latency != null}
val dotNotReachable = dotCount - dotReachable
val dohCount = testResults!!.size - dotCount
val dohReachable = testResults!!.count { it.server is HttpsDnsServerInformation && it.latency != null}
val dohNotReachable = dohCount - dohReachable
val avgLatency = testResults!!.sumBy { it.latency ?: 0 }/testResults!!.size
val fastestServer = testResults!!.minBy { it.latency ?: Integer.MAX_VALUE}
val slowestServer = testResults!!.minBy { it.latency ?: 0}
showInfoTextDialog(this,
getString(R.string.dialog_speedresult_title),
getString(R.string.dialog_speedresult_message,
testResults!!.size,
dotReachable,
dotNotReachable,
dohReachable,
dohNotReachable,
avgLatency,
fastestServer?.server?.name ?: "-",
slowestServer?.server?.name ?: "-"
))
}
}
serverList.layoutManager = LinearLayoutManager(this)
serverList.addItemDecoration(SpaceItemDecorator())
}
private fun startTest() {
serverList.adapter = null
testJob = launchWithLifecylce(false) {
val dnsServers = AbstractTLSDnsHandle.KNOWN_DNS_SERVERS.values +
AbstractHttpsDNSHandle.KNOWN_DNS_SERVERS.values +
getPreferences().userServers.map {
it.serverInformation
}
startTest.text = "0/${dnsServers.size}"
val testResults = dnsServers.map {
SpeedTest(it, null)
}.toMutableList()
this@SpeedTestActivity.testResults = testResults
val showUseServerDialog = { test:SpeedTest ->
showInfoTextDialog(this@SpeedTestActivity,
getString(R.string.dialog_speedtest_useserver_title),
getString(R.string.dialog_speedtest_useserver_message,
test.server.name,
testResults.indexOf(test) + 1,
testResults.size,
test.latency!!
),
getString(R.string.all_yes) to { dialog, _ ->
getPreferences().dnsServerConfig = test.server
dialog.dismiss()
}, getString(R.string.all_no) to { dialog, _ ->
dialog.dismiss()
}, null)
}
val adapter = AdapterBuilder.withViewHolder({ SpeedViewHolder(it, showUseServerDialog) }) {
viewBuilder = { parent, _ ->
layoutInflater.inflate(R.layout.item_dns_speed, parent, false)
}
getItemCount = {
testResults.size
}
bindView = { viewHolder, position ->
viewHolder.display(testResults[position])
}
}.build()
runOnUiThread {
serverList.adapter = adapter
}
val testsLeft = testResults.shuffled()
var cnt = 0
testsLeft.forEach {
if(!(testJob?.isCancelled ?: true)) {
it.started = true
val res = DnsSpeedTest(it.server, 500, 750).runTest(3)
if (res != null) it.latency = res
else it.error = true
testResults.sortBy {
it.latency ?: Integer.MAX_VALUE
}
runOnUiThread {
cnt++
adapter.notifyDataSetChanged()
startTest.text = "$cnt/${dnsServers.size}"
}
}
}
if(!(testJob?.isCancelled ?: true))runOnUiThread {
startTest.isEnabled = true
abort.visibility = View.GONE
startTest.text = getString(R.string.window_speedtest_runtest)
testRunning = false
info.visibility = View.VISIBLE
}
}
}
override fun getConfiguration(): Configuration {
return Configuration.withDefaults()
}
private inner class SpeedViewHolder(view: View, private val showUseServerDialog:(SpeedTest) -> Any) : BaseViewHolder(view) {
val name = view.name
val servers = view.servers
val progress = view.progress
val latency = view.latency
val serverType = view.serverType
val nameWrap = view.nameWrap
private var defaultTextColor = latency.currentTextColor
fun display(speedTest: SpeedTest) {
if(speedTest.latency != null) {
val listener:(View) ->Unit = { showUseServerDialog(speedTest) }
itemView.setOnClickListener(listener)
nameWrap.setOnClickListener(listener)
} else {
itemView.setOnClickListener(null)
nameWrap.setOnClickListener(null)
}
name.text = speedTest.server.name
servers.text = buildString {
speedTest.server.servers.forEach {
append(it.address.formatToString())
append("\n")
}
}
serverType.text = if(speedTest.server is HttpsDnsServerInformation) getString(R.string.tasker_mode_doh)
else getString(R.string.tasker_mode_dot)
if (speedTest.latency == null) {
when {
speedTest.error -> {
latency.text = "- ms"
latency.setTextColor(Color.RED)
progress.visibility = View.INVISIBLE
latency.visibility = View.VISIBLE
}
speedTest.started -> {
progress.visibility = View.VISIBLE
latency.visibility = View.INVISIBLE
}
else -> {
latency.text = "? ms"
latency.visibility = View.VISIBLE
progress.visibility = View.INVISIBLE
latency.setTextColor(defaultTextColor)
}
}
} else {
latency.text = "${speedTest.latency} ms"
latency.setTextColor(defaultTextColor)
progress.visibility = View.INVISIBLE
latency.visibility = View.VISIBLE
}
}
override fun destroy() {}
}
private class SpeedTest(val server: DnsServerInformation<*>, var latency: Int?) {
var error: Boolean = false
var started:Boolean = false
}
private inner class SpaceItemDecorator() : RecyclerView.ItemDecoration() {
private val decorationHeight: Int = DesignUtil.dpToPixels(12f, this@SpeedTestActivity).toInt()
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val itemPosition = parent.getChildAdapterPosition(view)
val totalCount = parent.adapter!!.itemCount
if (itemPosition >= 0 && itemPosition < totalCount - 1) {
outRect.bottom = decorationHeight
}
}
}
}
\ No newline at end of file
......@@ -5,10 +5,7 @@ import com.frostnerd.dnstunnelproxy.DnsServerInformation
import com.frostnerd.dnstunnelproxy.UpstreamAddress
import com.frostnerd.encrypteddnstunnelproxy.*
import com.frostnerd.encrypteddnstunnelproxy.tls.TLSUpstreamAddress
import okhttp3.Dns
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.*
import org.minidns.dnsmessage.DnsMessage
import org.minidns.dnsmessage.Question
import org.minidns.record.Record
......@@ -16,6 +13,8 @@ import java.io.DataInputStream
import java.io.DataOutputStream
import java.lang.Exception
import java.net.*
import java.time.Duration
import java.time.temporal.TemporalUnit
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLSocketFactory
import kotlin.random.Random
......@@ -39,11 +38,12 @@ import kotlin.random.Random
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class DnsSpeedTest(val server: DnsServerInformation<*>, val connectTimeout: Int = 2500) {
class DnsSpeedTest(val server: DnsServerInformation<*>, val connectTimeout: Int = 2500, val readTimeout:Int = 1500) {
private val httpClient by lazy {
OkHttpClient.Builder()
.dns(httpsDnsClient)
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(readTimeout.toLong(), TimeUnit.MILLISECONDS)
.build()
}
private val httpsDnsClient by lazy {
......@@ -61,6 +61,7 @@ class DnsSpeedTest(val server: DnsServerInformation<*>, val connectTimeout: Int
* @return The average response time (in ms)
*/
fun runTest(@IntRange(from = 1) passes: Int): Int? {
println("Running test for ${server.name}")
var ttl = 0
for (i in 0 until passes) {
if (server is HttpsDnsServerInformation) {
......@@ -91,9 +92,10 @@ class DnsSpeedTest(val server: DnsServerInformation<*>, val connectTimeout: Int
return null
}
}
var response:Response? = null
try {
val start = System.currentTimeMillis()
val response = httpClient.newCall(requestBuilder.build()).execute()
response = httpClient.newCall(requestBuilder.build()).execute()
if(!response.isSuccessful) return null
val body = response.body() ?: return null
val bytes = body.bytes()
......@@ -106,8 +108,9 @@ class DnsSpeedTest(val server: DnsServerInformation<*>, val connectTimeout: Int
}
return time
} catch (ex: Exception) {
ex.printStackTrace()
return null
} finally {
if(response?.body() != null) response.close()
}
}
......@@ -120,6 +123,7 @@ class DnsSpeedTest(val server: DnsServerInformation<*>, val connectTimeout: Int
val msg = createTestDnsPacket()
val start = System.currentTimeMillis()
socket.connect(InetSocketAddress(addr[0], address.port), connectTimeout)
socket.soTimeout = readTimeout
val data: ByteArray = msg.toArray()
val outputStream = DataOutputStream(socket.getOutputStream())
val size = data.size
......@@ -138,7 +142,6 @@ class DnsSpeedTest(val server: DnsServerInformation<*>, val connectTimeout: Int
if(!testResponse(DnsMessage(readData))) return null
return time
} catch (ex: Exception) {
ex.printStackTrace()
return null
} finally {
socket?.close()
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:animateLayoutChanges="true"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:titleTextColor="?android:attr/textColor"
android:theme="@style/AppTheme_Mono"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_above="@id/startTest"
android:layout_below="@id/toolBar"
android:layout_marginTop="12dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:id="@+id/serverList"
android:layout_marginBottom="12dp"
android:layout_height="wrap_content"/>
<Button
android:layout_width="match_parent"
android:id="@+id/startTest"
android:layout_centerHorizontal="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/main_roundbuttons"
android:layout_alignParentBottom="true"
android:layout_toStartOf="@id/abort"
android:layout_marginBottom="16dp"
android:text="@string/window_speedtest_runtest"
android:layout_height="wrap_content"/>
<ImageButton
android:id="@+id/abort"
android:src="@drawable/ic_times"
android:background="@drawable/main_roundbuttons"
android:tint="?android:attr/textColor"
android:layout_width="48dp"
android:layout_alignParentBottom="true"
android:visibility="gone"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:layout_toStartOf="@id/info"
android:contentDescription="@string/contentdescription_main_servericon"
android:layout_alignParentEnd="true"
android:layout_height="48dp"/>
<ImageButton
android:id="@+id/info"
android:src="@drawable/ic_info"
android:background="@drawable/main_roundbuttons"
android:tint="?android:attr/textColor"
android:layout_width="48dp"
android:layout_alignParentBottom="true"
android:visibility="gone"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/contentdescription_main_servericon"
android:layout_alignParentEnd="true"
android:layout_height="48dp"/>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
app:cardElevation="8dp"
app:cardCornerRadius="12dp"
android:background="?selectableItemBackground"
android:layout_marginRight="4dp"
app:cardBackgroundColor="?inputElementColor"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_height="wrap_content">
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/resultWrap"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:orientation="vertical"
android:id="@+id/nameWrap"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:textStyle="bold"
android:textSize="18sp"
android:id="@+id/name"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:id="@+id/servers"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:id="@+id/serverType"
android:layout_height="wrap_content"/>
</LinearLayout>
</HorizontalScrollView>
<RelativeLayout
android:layout_width="wrap_content"
android:id="@+id/resultWrap"
android:layout_marginStart="12dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:textStyle="bold"
android:textSize="18sp"
android:id="@+id/latency"
android:visibility="invisible"
android:text="9999 ms"
android:layout_centerInParent="true"
android:layout_height="wrap_content"/>
<ProgressBar
android:layout_width="48dp"
android:id="@+id/progress"
android:visibility="invisible"
android:layout_centerInParent="true"
android:layout_height="48dp"/>
</RelativeLayout>
</RelativeLayout>
</androidx.cardview.widget.CardView>
\ No newline at end of file
......@@ -97,4 +97,17 @@
<string name="dialog_help_translating_message" translatable="false">This app is translated to a few languages, but even more are missing or incomplete - including yours.
\n\nThat\'s why I\'m kindly asking for your help translating this app to reach as broad of an audience as possible. If you are willing to help translating this app into your language feel free to either contact me or <![CDATA[<a href="https://git.frostnerd.com/PublicAndroidApps/smokescreen/blob/master/TRANSLATING.md">read here on how to help.</a>]]></string>
<string name="dialog_speedresult_title">Speed test results</string>
<string name="dialog_speedresult_message">
%1d servers tested\n
%2d DoT servers reachable, %3d not reachable\n
%4d DoH servers reachable, %5d not reachable\n
Average response time: %6d ms.\n\n
Fastest server: %7s\n
Slowest server: %8s
</string>
<string name="dialog_speedtest_useserver_title">Select server</string>
<string name="dialog_speedtest_useserver_message">Do you want to use the server %1s? It ranked %2d out of %3d with a response time of %4d ms.</string>
</resources>
\ No newline at end of file
......@@ -3,6 +3,7 @@
<string name="activity_label_import_queries">Import queries</string>
<string name="activity_label_import_servers">Import server(s)</string>
<string name="activity_label_speed_test">Server speed comparison</string>
<string name="activity_label_enable_logging">Export logs/Enable logging</string>
......@@ -56,4 +57,7 @@
<string name="about_info">General information</string>
<string name="about_help_translating">Help translating</string>
<string name="window_speedtest_runtest">Start the test</string>
<string name="window_speedtest_useserver">Use this server</string>
</resources>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment