Rayan545454's picture
<?xml version="1.0" encoding="utf-8"?> <manifest package="com.example.wifitesterpro" xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> ‎ <!-- لعرض SSID بدقة على Android 10+ --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <application android:allowBackup="true" android:label="Wi-Fi Tester Pro" android:supportsRtl="true" android:theme="@style/Theme.WifiTesterPro" android:usesCleartextTraffic="true"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> // Top-level build file plugins { id 'com.android.application' version '8.6.0' apply false id 'org.jetbrains.kotlin.android' version '1.9.24' apply false } pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "WifiTesterPro" include ':app'plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } android { namespace 'com.example.wifitesterpro' compileSdk 34 defaultConfig { applicationId "com.example.wifitesterpro" minSdk 26 targetSdk 34 versionCode 1 versionName "1.0.0" } buildFeatures { viewBinding true } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } } dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1" implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' } <resources xmlns:tools="http://schemas.android.com/tools"> <style name="Theme.WifiTesterPro" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <item name="android:statusBarColor">?attr/colorPrimaryVariant</item> </style> </resources> <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <TextView android:text="اختبار شبكة Wi-Fi (نسخة مطوّرة)" android:textSize="20sp" android:textStyle="bold" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <EditText android:id="@+id/etHost" android:hint="الهوست للاختبار (مثال: 8.8.8.8 أو www.example.com)" android:inputType="textUri" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp"/> <EditText android:id="@+id/etPorts" android:hint="منافذ لفحصها (مثال: 53,80,443,8080)" android:inputType="text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp"/> <EditText android:id="@+id/etUrl" android:hint="رابط HTTP/HTTPS لاختبار الاستجابة (مثال: https://www.google.com)" android:inputType="textUri" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp"/> <Button android:id="@+id/btnRunAll" android:text="تشغيل كل الاختبارات" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp"/> <Button android:id="@+id/btnExport" android:text="تصدير النتائج CSV" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp"/> <TextView android:id="@+id/tvInfo" android:text="معلومات الشبكة..." android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp"/> <TextView android:id="@+id/tvResults" android:text="نتائج الاختبارات..." android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp"/> </LinearLayout> </ScrollView> package com.example.wifitesterpro import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.net.wifi.WifiManager import android.text.format.Formatter import kotlinx.coroutines.* import java.io.BufferedInputStream import java.net.HttpURLConnection import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket import java.net.URL import java.util.concurrent.TimeUnit import kotlin.math.roundToInt data class WifiInfoData( val ssid: String?, val ip: String?, val gateway: String?, val linkSpeedMbps: Int?, val rssi: Int?, val frequencyMhz: Int? ) data class PingStats( val transmitted: Int, val received: Int, val lossPercent: Double, val minMs: Double?, val avgMs: Double?, val maxMs: Double? ) object NetworkTester { fun getWifiInfo(context: Context): WifiInfoData { val wifi = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager val conn = wifi.connectionInfo val dhcp = wifi.dhcpInfo val ip = dhcp?.let { Formatter.formatIpAddress(it.ipAddress) } val gateway = dhcp?.let { Formatter.formatIpAddress(it.gateway) } return WifiInfoData( ssid = conn?.ssid?.replace("\"",""), ip = ip, gateway = gateway, linkSpeedMbps = conn?.linkSpeed, rssi = conn?.rssi, frequencyMhz = conn?.frequency ) } fun isConnectedToInternet(context: Context): Boolean { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val nw = cm.activeNetwork ?: return false val cap = cm.getNetworkCapabilities(nw) ?: return false return cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } suspend fun pingRaw(host: String, count: Int = 4, timeoutSec: Int = 4): String = withContext(Dispatchers.IO) { try { val proc = ProcessBuilder() .command("ping", "-c", count.toString(), "-W", timeoutSec.toString(), host) .redirectErrorStream(true) .start() proc.waitFor((timeoutSec * (count + 1)).toLong(), TimeUnit.SECONDS) proc.inputStream.bufferedReader().use { it.readText() } } catch (e: Exception) { "Ping error: ${e.message}" } } fun parsePingStats(output: String, fallbackCount: Int): PingStats { // يحاول استخراج الإحصائيات من مخرجات ping القياسية val tx = Regex("(\\d+) packets transmitted").find(output)?.groupValues?.get(1)?.toIntOrNull() ?: fallbackCount val rx = Regex("(\\d+) received").find(output)?.groupValues?.get(1)?.toIntOrNull() ?: 0 val loss = Regex("(\\d+(?:\\.\\d+)?)% packet loss").find(output)?.groupValues?.get(1)?.toDoubleOrNull() ?: run { if (tx > 0) ((tx - rx).toDouble() / tx) * 100.0 else 100.0 } val rtt = Regex("min/avg/max/(?:mdev|stddev) = ([\\d.]+)/([\\d.]+)/([\\d.]+)/").find(output) val minMs = rtt?.groupValues?.get(1)?.toDoubleOrNull() val avgMs = rtt?.groupValues?.get(2)?.toDoubleOrNull() val maxMs = rtt?.groupValues?.get(3)?.toDoubleOrNull() return PingStats(tx, rx, loss, minMs, avgMs, maxMs) } suspend fun tcpConnect(host: String, port: Int, timeoutMs: Int = 3000): Pair<Boolean, Long> = withContext(Dispatchers.IO) { val start = System.nanoTime() return@withContext try { Socket().use { s -> s.connect(InetSocketAddress(host, port), timeoutMs) } val ms = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start) true to ms } catch (_: Exception) { val ms = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start) false to ms } } suspend fun scanPorts(host: String, ports: List<Int>, timeoutMs: Int = 1000): List<String> = coroutineScope { ports.map { p -> async(Dispatchers.IO) { val (ok, ms) = tcpConnect(host, p, timeoutMs) "${host}:${p} -> ${if (ok) "OPEN" else "CLOSED"} (${ms}ms)" } }.awaitAll() } suspend fun httpGet(urlStr: String, timeoutMs: Int = 6000): String = withContext(Dispatchers.IO) { try { val url = URL(urlStr) val conn = (url.openConnection() as HttpURLConnection).apply { connectTimeout = timeoutMs readTimeout = timeoutMs requestMethod = "GET" } val start = System.nanoTime() val code = conn.responseCode val latency = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start) conn.inputStream?.close() "HTTP GET $urlStr → $code (${latency}ms)" } catch (e: Exception) { "HTTP GET $urlStr: error ${e.message}" } } suspend fun captivePortalCheck(): String = httpGet("http://clients3.google.com/generate_204", 5000) suspend fun downloadSpeedKbps( testUrl: String = "https://speed.hetzner.de/100MB.bin", bytesToRead: Long = 2_000_000 ): String = withContext(Dispatchers.IO) { try { val url = URL(testUrl) val conn = (url.openConnection() as HttpURLConnection).apply { connectTimeout = 5000 readTimeout = 8000 requestMethod = "GET" } var readTotal = 0L val start = System.nanoTime() BufferedInputStream(conn.inputStream).use { input -> val buf = ByteArray(64 * 1024) var n: Int while (readTotal < bytesToRead && input.read(buf).also { n = it } != -1) { readTotal += n } } val elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start).coerceAtLeast(1) val kbps = (readTotal / 1024.0) / (elapsedMs / 1000.0) "Estimated download: %.1f KB/s (%.2f MB in %d ms)".format(kbps, readTotal/1_000_000.0, elapsedMs) } catch (e: Exception) { "Download test error: ${e.message}" } } suspend fun dnsLookup(host: String): String = withContext(Dispatchers.IO) { try { val addr = InetAddress.getByName(host) "DNS $host → ${addr.hostAddress}" } catch (e: Exception) { "DNS $host: error ${e.message}" } } } package com.example.wifitesterpro import android.content.ContentValues import android.content.Context import android.os.Build import android.provider.MediaStore import java.io.OutputStream import java.nio.charset.Charset import java.text.SimpleDateFormat import java.util.* object CsvExporter { fun saveToDownloads(context: Context, filePrefix: String, csvContent: String): String { val ts = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) val filename = "${filePrefix}_${ts}.csv" val resolver = context.contentResolver val collection = if (Build.VERSION.SDK_INT >= 29) { MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) } else { MediaStore.Files.getContentUri("external") } val values = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, filename) put(MediaStore.MediaColumns.MIME_TYPE, "text/csv") } val uri = resolver.insert(collection, values) ?: throw IllegalStateException("Failed to create file in Downloads") resolver.openOutputStream(uri)?.use { out: OutputStream -> out.write(csvContent.toByteArray(Charset.forName("UTF-8"))) out.flush() } ?: throw IllegalStateException("Failed to open output stream") return filename } } package com.example.wifitesterpro import android.Manifest import android.content.pm.PackageManager import android.os.Bundle import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.example.wifitesterpro.databinding.ActivityMainBinding import kotlinx.coroutines.* class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val requestLocationPermission = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (!granted) { Toast.makeText(this, "لإظهار SSID بدقة على Android 10+، فعّل إذن الموقع.", Toast.LENGTH_LONG).show() } showWifiInfo() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.btnRunAll.setOnClickListener { runAllTests() } binding.btnExport.setOnClickListener { exportCsv() } maybeAskLocation() showWifiInfo() } private fun maybeAskLocation() { val perm = Manifest.permission.ACCESS_FINE_LOCATION if (ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) { requestLocationPermission.launch(perm) } } private fun showWifiInfo() { val info = NetworkTester.getWifiInfo(this) val connected = NetworkTester.isConnectedToInternet(this) val text = buildString { appendLine("الحالة: ${if (connected) "متصل بالإنترنت" else "غير متصل"}") appendLine("SSID: ${info.ssid ?: "غير متاح"}") appendLine("IP: ${info.ip ?: "غير متاح"}") appendLine("Gateway: ${info.gateway ?: "غير متاح"}") appendLine("Link Speed: ${info.linkSpeedMbps ?: "?"} Mbps") appendLine("RSSI: ${info.rssi ?: "?"} dBm") appendLine("Frequency: ${info.frequencyMhz ?: "?"} MHz") } binding.tvInfo.text = text } private fun runAllTests() { val defaultHost = NetworkTester.getWifiInfo(this).gateway ?: "8.8.8.8" val host = binding.etHost.text.toString().ifBlank { defaultHost } val ports = binding.etPorts.text.toString().ifBlank { "53,80,443" } .split(",").mapNotNull { it.trim().toIntOrNull() }.distinct() val url = binding.etUrl.text.toString().ifBlank { "https://www.google.com/generate_204" } binding.tvResults.text = "جاري التنفيذ..." scope.launch { val lines = mutableListOf<String>() lines += "=== General ===" lines += NetworkTester.captivePortalCheck() lines += "\n=== DNS ===" lines += NetworkTester.dnsLookup(host) lines += "\n=== Ping ===" val raw = NetworkTester.pingRaw(host, count = 5, timeoutSec = 3) val stats = NetworkTester.parsePingStats(raw, 5) lines += "Ping $host → tx=${stats.transmitted}, rx=${stats.received}, loss=${"%.1f".format(stats.lossPercent)}%" lines += "rtt min/avg/max = ${stats.minMs ?: "-"} / ${stats.avgMs ?: "-"} / ${stats.maxMs ?: "-"} ms" lines += "\n=== TCP Connect ===" val (ok443, ms443) = NetworkTester.tcpConnect(host, 443, 3000) lines += "TCP $host:443 → ${if (ok443) "OPEN" else "CLOSED"} (${ms443}ms)" if (ports.isNotEmpty()) { lines += "\n=== Port Scan (${host}) ===" NetworkTester.scanPorts(host, ports, 1200).forEach { lines += it } } lines += "\n=== HTTP Test ===" lines += NetworkTester.httpGet(url, 6000) lines += "\n=== Download Speed (est.) ===" lines += NetworkTester.downloadSpeedKbps() binding.tvResults.text = lines.joinToString("\n") } } private fun exportCsv() { val infoText = binding.tvInfo.text.toString() val resultsText = binding.tvResults.text.toString() if (resultsText.isBlank()) { Toast.makeText(this, "لا توجد نتائج لتصديرها.", Toast.LENGTH_SHORT).show() return } val csv = buildString { appendLine("Section,Key,Value") infoText.lines().forEach { line -> val parts = line.split(":", limit = 2) if (parts.size == 2) { appendLine("Info,${parts[0].trim()},${parts[1].trim()}") } } appendLine("Results,Raw,\"\"\"${resultsText.replace("\"","\"\"")}\"\"\"") } try { val name = CsvExporter.saveToDownloads(this, "wifi_tester_results", csv) Toast.makeText(this, "تم الحفظ في التنزيلات: $name", Toast.LENGTH_LONG).show() } catch (e: Exception) { Toast.makeText(this, "فشل الحفظ: ${e.message}", Toast.LENGTH_LONG).show() } } override fun onDestroy() { super.onDestroy() scope.cancel() } } - Initial Deployment
0e5795e verified