Skip to content

Commit

Permalink
Android GUI enhancement (#34)
Browse files Browse the repository at this point in the history
* custom config.

* custom config.

* random ip ranges

* remove unused tip

* add app icon

* fix workflow.
  • Loading branch information
peanut996 authored Dec 23, 2024
1 parent 6e5fb23 commit 7cedcce
Show file tree
Hide file tree
Showing 13 changed files with 716 additions and 118 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,18 @@ jobs:
env:
BUILD_TOOLS_VERSION: "34.0.0"

- name: Rename APK with Git SHA
if: startsWith(github.ref, 'refs/tags/v')
run: |
GIT_SHA=$(git rev-parse --short HEAD)
mv ${{ env.SIGNED_RELEASE_FILE }} android/app/build/outputs/apk/release/WARPTest-${GIT_SHA}.apk
echo "RELEASE_APK=android/app/build/outputs/apk/release/WARPTest-${GIT_SHA}.apk" >> $GITHUB_ENV
- name: Create Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: |
${{ env.SIGNED_RELEASE_FILE }}
${{ env.RELEASE_APK }}
draft: true
prerelease: true

Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.peanut996.cloudflarewarpspeedtest.models.SpeedTestConfig
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.peanut996.cloudflarewarpspeedtest.models.SpeedTestConfigBuilder
import com.peanut996.cloudflarewarpspeedtest.models.SpeedTestResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
Expand All @@ -18,35 +19,50 @@ import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var speedTest: SpeedTest
private lateinit var resultTextView: TextView
private lateinit var progressTextView: TextView
private lateinit var progressBar: ProgressBar
private lateinit var startButton: Button
private lateinit var stopButton: Button
private lateinit var ipPortTextView: TextView
private lateinit var settingsButton: View
private lateinit var resultRecyclerView: RecyclerView
private lateinit var resultHeader: View
private lateinit var resultAdapter: SpeedTestResultAdapter
private var currentConfig = SpeedTestConfigBuilder.createDefault()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

speedTest = SpeedTest()
setupViews()
setupRecyclerView()
setupClickListeners()
configureSpeedTest()
}

private fun setupViews() {
resultTextView = findViewById(R.id.resultText)
progressTextView = findViewById(R.id.progressText)
progressBar = findViewById(R.id.progressBar)
startButton = findViewById(R.id.startButton)
stopButton = findViewById(R.id.stopButton)
ipPortTextView = findViewById(R.id.ipPortText)
settingsButton = findViewById(R.id.settingsButton)
resultRecyclerView = findViewById(R.id.resultRecyclerView)
resultHeader = findViewById(R.id.resultHeader)

// Initially disable stop button and hide progress
stopButton.isEnabled = false
progressBar.visibility = View.GONE
progressTextView.visibility = View.GONE
resultHeader.visibility = View.GONE
resultRecyclerView.visibility = View.GONE
}

private fun setupRecyclerView() {
resultAdapter = SpeedTestResultAdapter()
resultRecyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = resultAdapter
setHasFixedSize(true)
}
}

private fun setupClickListeners() {
Expand All @@ -57,15 +73,27 @@ class MainActivity : AppCompatActivity() {
stopButton.setOnClickListener {
stopSpeedTest()
}

settingsButton.setOnClickListener {
showConfigDialog()
}
}

private fun showConfigDialog() {
SpeedTestConfigDialog.newInstance(currentConfig).apply {
setOnConfigUpdatedListener { config ->
currentConfig = config
configureSpeedTest()
}
}.show(supportFragmentManager, "config_dialog")
}

private fun configureSpeedTest() {
val config = SpeedTestConfig()
try {
speedTest.configure(config)
speedTest.configure(currentConfig)
} catch (e: Exception) {
Log.e(TAG, "Failed to configure speed test: ${e.message}")
resultTextView.text = "Configuration error: ${e.message}"
progressTextView.text = "Configuration error: ${e.message}"
}
}

Expand All @@ -74,9 +102,11 @@ class MainActivity : AppCompatActivity() {
try {
startButton.isEnabled = false
stopButton.isEnabled = true
progressBar.visibility = View.VISIBLE
progressTextView.visibility = View.VISIBLE
resultTextView.text = "Starting speed test..."
resultAdapter.clearResults()
resultHeader.visibility = View.GONE
resultRecyclerView.visibility = View.GONE
ipPortTextView.text = ""

speedTest.start()

Expand All @@ -101,12 +131,11 @@ class MainActivity : AppCompatActivity() {
}
}
} while (finalResult != null)

} catch (e: Exception) {
Log.e(TAG, "Error during speed test: ${e.message}")
withContext(Dispatchers.Main) {
resultTextView.text = "Error: ${e.message}"
progressBar.visibility = View.GONE
progressTextView.visibility = View.GONE
progressTextView.text = "Error: ${e.message}"
}
} finally {
withContext(Dispatchers.Main) {
Expand All @@ -119,55 +148,28 @@ class MainActivity : AppCompatActivity() {

private fun stopSpeedTest() {
speedTest.stop()
startButton.isEnabled = true
stopButton.isEnabled = false
progressBar.visibility = View.GONE
progressTextView.visibility = View.GONE
resultTextView.text = "Speed test stopped"
resultHeader.visibility = View.GONE
resultRecyclerView.visibility = View.GONE
progressTextView.text = "Speed test stopped"
}

private fun updateProgress(result: Any) {
when (result) {
is String -> {
progressTextView.text = result
}
is List<*> -> {
@Suppress("UNCHECKED_CAST")
val results = result as List<SpeedTestResult>
resultTextView.text = formatResults(results)
progressBar.visibility = View.GONE
progressTextView.visibility = View.GONE
}
is String -> {
val progressMatch = Regex("Progress: (\\d+)/(\\d+) \\((\\d+)%\\)").find(result)
if (progressMatch != null) {
val (current, total, percentage) = progressMatch.destructured
progressBar.progress = percentage.toInt()
progressTextView.text = "Testing endpoints: $current/$total ($percentage%)"
} else if (result.startsWith("Found working endpoint")) {
// Append the new endpoint to existing text while keeping progress
val currentText = resultTextView.text.toString()
resultTextView.text = if (currentText.startsWith("Starting")) {
result + "\n"
} else {
currentText + result + "\n"
}
} else {
resultTextView.text = result
val speedTestResults = result as List<SpeedTestResult>
resultAdapter.updateResults(speedTestResults)
if (speedTestResults.isNotEmpty()) {
progressTextView.visibility = View.GONE
resultHeader.visibility = View.VISIBLE
resultRecyclerView.visibility = View.VISIBLE
}
}
}
}

private fun formatResults(results: List<SpeedTestResult>): String {
if (results.isEmpty()) {
return "No results yet"
}

return buildString {
appendLine("Test Results:")
results.forEachIndexed { index, result ->
appendLine("${index + 1}. IP: ${result.ip}:${result.port}")
appendLine(" Delay: ${result.delay}ms")
appendLine(" Loss Rate: ${String.format("%.2f", result.lossRate * 100)}%")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ class SpeedTest {

val ip = "${baseIPParts[0]}.${baseIPParts[1]}.${baseIPParts[2]}.$lastOctet"
ipRanges.add(ip)

if (ipRanges.size >= config.maxScanCount) break
}
} catch (e: Exception) {
Log.e(TAG, "Error processing CIDR $cidr: ${e.message}")
Expand All @@ -103,7 +101,7 @@ class SpeedTest {
} else emptyList()

// TODO: Add IPv6 support
return ipRanges.take(config.maxScanCount)
return ipRanges
}

fun start() {
Expand All @@ -118,9 +116,11 @@ class SpeedTest {
val ipRanges = loadWarpIPRanges()
val results = mutableListOf<SpeedTestResult>()
var testedCount = 0
val endpoints = ipRanges.flatMap { ip ->
val allEndpoints = ipRanges.flatMap { ip ->
ports.map { port -> Pair(ip, port) }
}.take(config.maxScanCount)
}
// Randomly select maxScanCount endpoints
val endpoints = allEndpoints.shuffled().take(config.maxScanCount)
val totalCount = endpoints.size
Log.d(TAG, "total count: $totalCount")

Expand All @@ -132,7 +132,6 @@ class SpeedTest {
// Calculate percentage and update progress
val percentage = (testedCount.toFloat() / totalCount.toFloat() * 100).toInt()
launch {
resultQueue.send("Progress: $testedCount/$totalCount ($percentage%)")
// Send intermediate result if all tests are complete
if (testedCount == totalCount) {
if (results.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.peanut996.cloudflarewarpspeedtest

import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.RadioButton
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.peanut996.cloudflarewarpspeedtest.models.SpeedTestConfig
import com.peanut996.cloudflarewarpspeedtest.models.SpeedTestConfigBuilder

class SpeedTestConfigDialog : DialogFragment() {
private var currentConfig = SpeedTestConfigBuilder.createDefault()
private var onConfigUpdated: ((SpeedTestConfig) -> Unit)? = null

private lateinit var threadCountInput: TextInputEditText
private lateinit var pingTimesInput: TextInputEditText
private lateinit var maxScanCountInput: TextInputEditText
private lateinit var maxDelayInput: TextInputEditText
private lateinit var minDelayInput: TextInputEditText
private lateinit var maxLossRateInput: TextInputEditText
private lateinit var resultDisplayCountInput: TextInputEditText
private lateinit var testAllCombosCheckbox: CheckBox
private lateinit var ipv6ModeCheckbox: CheckBox
private lateinit var defaultConfigRadio: RadioButton
private lateinit var fastConfigRadio: RadioButton
private lateinit var accurateConfigRadio: RadioButton

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val view = LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_speed_test_config, null)

initializeViews(view)
setupPresetConfigs()
loadCurrentConfig()

return MaterialAlertDialogBuilder(requireContext())
.setTitle("Speed Test Configuration")
.setView(view)
.setPositiveButton("Apply") { _, _ -> applyConfig() }
.setNegativeButton("Cancel", null)
.create()
}

private fun initializeViews(view: View) {
threadCountInput = view.findViewById(R.id.threadCountInput)
pingTimesInput = view.findViewById(R.id.pingTimesInput)
maxScanCountInput = view.findViewById(R.id.maxScanCountInput)
maxDelayInput = view.findViewById(R.id.maxDelayInput)
minDelayInput = view.findViewById(R.id.minDelayInput)
maxLossRateInput = view.findViewById(R.id.maxLossRateInput)
resultDisplayCountInput = view.findViewById(R.id.resultDisplayCountInput)
testAllCombosCheckbox = view.findViewById(R.id.testAllCombosCheckbox)
ipv6ModeCheckbox = view.findViewById(R.id.ipv6ModeCheckbox)
defaultConfigRadio = view.findViewById(R.id.defaultConfigRadio)
fastConfigRadio = view.findViewById(R.id.fastConfigRadio)
accurateConfigRadio = view.findViewById(R.id.accurateConfigRadio)
}

private fun setupPresetConfigs() {
defaultConfigRadio.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) updateInputs(SpeedTestConfigBuilder.createDefault())
}
fastConfigRadio.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) updateInputs(SpeedTestConfigBuilder.createFastConfig())
}
accurateConfigRadio.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) updateInputs(SpeedTestConfigBuilder.createAccurateConfig())
}
}

private fun loadCurrentConfig() {
updateInputs(currentConfig)
}

private fun updateInputs(config: SpeedTestConfig) {
threadCountInput.setText(config.threadCount.toString())
pingTimesInput.setText(config.pingTimes.toString())
maxScanCountInput.setText(config.maxScanCount.toString())
maxDelayInput.setText(config.maxDelay.toString())
minDelayInput.setText(config.minDelay.toString())
maxLossRateInput.setText(config.maxLossRate.toString())
resultDisplayCountInput.setText(config.resultDisplayCount.toString())
testAllCombosCheckbox.isChecked = config.testAllCombos
ipv6ModeCheckbox.isChecked = config.ipv6Mode
}

private fun applyConfig() {
try {
val builder = SpeedTestConfigBuilder()
.setThreadCount(threadCountInput.text.toString().toInt())
.setPingTimes(pingTimesInput.text.toString().toInt())
.setMaxScanCount(maxScanCountInput.text.toString().toInt())
.setMaxDelay(maxDelayInput.text.toString().toInt())
.setMinDelay(minDelayInput.text.toString().toInt())
.setMaxLossRate(maxLossRateInput.text.toString().toDouble())
.setResultDisplayCount(resultDisplayCountInput.text.toString().toInt())
.setTestAllCombos(testAllCombosCheckbox.isChecked)
.setIpv6Mode(ipv6ModeCheckbox.isChecked)

val newConfig = builder.build()
currentConfig = newConfig
onConfigUpdated?.invoke(newConfig)
} catch (e: Exception) {
Toast.makeText(context, "Invalid input: ${e.message}", Toast.LENGTH_LONG).show()
}
}

fun setConfig(config: SpeedTestConfig) {
currentConfig = config
if (this::threadCountInput.isInitialized) {
loadCurrentConfig()
}
}

fun setOnConfigUpdatedListener(listener: (SpeedTestConfig) -> Unit) {
onConfigUpdated = listener
}

companion object {
fun newInstance(config: SpeedTestConfig): SpeedTestConfigDialog {
return SpeedTestConfigDialog().apply {
currentConfig = config
}
}
}
}
Loading

0 comments on commit 7cedcce

Please sign in to comment.