1
app/.gitignore
vendido
Archivo normal
@@ -0,0 +1 @@
|
||||
/build
|
||||
62
app/build.gradle.kts
Archivo normal
@@ -0,0 +1,62 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.manalejandro.wifiattack"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.manalejandro.wifiattack"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
21
app/proguard-rules.pro
vendido
Archivo normal
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.manalejandro.wifiattack
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.manalejandro.wifiattack", appContext.packageName)
|
||||
}
|
||||
}
|
||||
46
app/src/main/AndroidManifest.xml
Archivo normal
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- WiFi permissions -->
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- Location permissions (required for WiFi scanning on Android 6.0+) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<!-- Foreground service permission for continuous monitoring -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Hardware features -->
|
||||
<uses-feature android:name="android.hardware.wifi" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.sensor.compass" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.sensor.accelerometer" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WifiAttack">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.WifiAttack">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
345
app/src/main/java/com/manalejandro/wifiattack/MainActivity.kt
Archivo normal
@@ -0,0 +1,345 @@
|
||||
package com.manalejandro.wifiattack
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Place
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.manalejandro.wifiattack.presentation.WifiAttackViewModel
|
||||
import com.manalejandro.wifiattack.presentation.screens.AttacksScreen
|
||||
import com.manalejandro.wifiattack.presentation.screens.ChannelStatsScreen
|
||||
import com.manalejandro.wifiattack.presentation.screens.DashboardScreen
|
||||
import com.manalejandro.wifiattack.presentation.screens.DirectionScreen
|
||||
import com.manalejandro.wifiattack.ui.theme.WifiAttackTheme
|
||||
|
||||
/**
|
||||
* Main Activity for the WiFi Attack Detector application.
|
||||
* Handles permissions and hosts the main Compose UI.
|
||||
*/
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private val requiredPermissions = mutableListOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.ACCESS_WIFI_STATE,
|
||||
Manifest.permission.CHANGE_WIFI_STATE
|
||||
).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
add(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
|
||||
private var onPermissionResult: ((Boolean) -> Unit)? = null
|
||||
|
||||
private val permissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { permissions ->
|
||||
val allGranted = permissions.values.all { it }
|
||||
onPermissionResult?.invoke(allGranted)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
WifiAttackTheme {
|
||||
val viewModel: WifiAttackViewModel = viewModel()
|
||||
|
||||
var permissionsChecked by remember { mutableStateOf(false) }
|
||||
var showRationale by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
checkAndRequestPermissions { granted ->
|
||||
viewModel.setPermissionsGranted(granted)
|
||||
permissionsChecked = true
|
||||
if (!granted) {
|
||||
showRationale = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showRationale) {
|
||||
PermissionRationaleScreen(
|
||||
onRequestPermission = {
|
||||
showRationale = false
|
||||
requestPermissions { granted ->
|
||||
viewModel.setPermissionsGranted(granted)
|
||||
if (!granted) {
|
||||
showRationale = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
WifiAttackApp(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAndRequestPermissions(onResult: (Boolean) -> Unit) {
|
||||
val notGranted = requiredPermissions.filter {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (notGranted.isEmpty()) {
|
||||
onResult(true)
|
||||
} else {
|
||||
requestPermissions(onResult)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPermissions(onResult: (Boolean) -> Unit) {
|
||||
onPermissionResult = onResult
|
||||
permissionLauncher.launch(requiredPermissions.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PermissionRationaleScreen(
|
||||
onRequestPermission: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Scaffold { paddingValues ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "Permissions Required",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "WiFi Attack Detector needs the following permissions to function:",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
PermissionItem(
|
||||
icon = Icons.Default.LocationOn,
|
||||
title = "Location Access",
|
||||
description = "Required to scan WiFi networks (Android requirement)"
|
||||
)
|
||||
|
||||
PermissionItem(
|
||||
icon = Icons.Default.Settings,
|
||||
title = "WiFi Access",
|
||||
description = "Required to monitor WiFi network activity"
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = onRequestPermission,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Grant Permissions")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionItem(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
title: String,
|
||||
description: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WifiAttackApp(
|
||||
viewModel: WifiAttackViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val selectedTab by viewModel.selectedTab.collectAsState()
|
||||
val isScanning by viewModel.isScanning.collectAsState()
|
||||
val networks by viewModel.networks.collectAsState()
|
||||
val channelStats by viewModel.channelStats.collectAsState()
|
||||
val attacks by viewModel.attacks.collectAsState()
|
||||
val lastScanTime by viewModel.lastScanTime.collectAsState()
|
||||
val azimuth by viewModel.azimuth.collectAsState()
|
||||
val signalDirection by viewModel.signalDirection.collectAsState()
|
||||
val signalStrengthAtDirection by viewModel.signalStrengthAtDirection.collectAsState()
|
||||
val isTrackingDirection by viewModel.isTrackingDirection.collectAsState()
|
||||
val selectedNetwork by viewModel.selectedNetwork.collectAsState()
|
||||
val isSensorAvailable by viewModel.isSensorAvailable.collectAsState()
|
||||
val activeAttacksCount by viewModel.activeAttacksCount.collectAsState()
|
||||
val highestThreatLevel by viewModel.highestThreatLevel.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("WiFi Attack Detector") },
|
||||
actions = {
|
||||
IconButton(onClick = { viewModel.toggleScanning() }) {
|
||||
Icon(
|
||||
imageVector = if (isScanning) Icons.Default.Close else Icons.Default.PlayArrow,
|
||||
contentDescription = if (isScanning) "Stop monitoring" else "Start monitoring"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Home, contentDescription = null) },
|
||||
label = { Text("Dashboard") },
|
||||
selected = selectedTab == 0,
|
||||
onClick = { viewModel.selectTab(0) }
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (channelStats.any { it.hasSuspiciousActivity }) {
|
||||
Badge()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.List, contentDescription = null)
|
||||
}
|
||||
},
|
||||
label = { Text("Channels") },
|
||||
selected = selectedTab == 1,
|
||||
onClick = { viewModel.selectTab(1) }
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (attacks.isNotEmpty()) {
|
||||
Badge { Text(attacks.size.toString()) }
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.Warning, contentDescription = null)
|
||||
}
|
||||
},
|
||||
label = { Text("Attacks") },
|
||||
selected = selectedTab == 2,
|
||||
onClick = { viewModel.selectTab(2) }
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Place, contentDescription = null) },
|
||||
label = { Text("Direction") },
|
||||
selected = selectedTab == 3,
|
||||
onClick = { viewModel.selectTab(3) }
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (selectedTab) {
|
||||
0 -> DashboardScreen(
|
||||
isScanning = isScanning,
|
||||
networksCount = networks.size,
|
||||
activeAttacksCount = activeAttacksCount,
|
||||
highestThreatLevel = highestThreatLevel,
|
||||
channelStats = channelStats,
|
||||
recentAttacks = attacks,
|
||||
lastScanTime = lastScanTime,
|
||||
onToggleScanning = { viewModel.toggleScanning() },
|
||||
onNavigateToChannels = { viewModel.selectTab(1) },
|
||||
onNavigateToAttacks = { viewModel.selectTab(2) }
|
||||
)
|
||||
1 -> ChannelStatsScreen(
|
||||
channelStats = channelStats,
|
||||
isScanning = isScanning
|
||||
)
|
||||
2 -> AttacksScreen(
|
||||
attacks = attacks,
|
||||
onClearHistory = { viewModel.clearAttackHistory() }
|
||||
)
|
||||
3 -> DirectionScreen(
|
||||
networks = networks,
|
||||
azimuth = azimuth,
|
||||
signalDirection = signalDirection,
|
||||
signalStrengthAtDirection = signalStrengthAtDirection,
|
||||
isTrackingDirection = isTrackingDirection,
|
||||
selectedNetwork = selectedNetwork,
|
||||
isSensorAvailable = isSensorAvailable,
|
||||
onStartTracking = { network -> viewModel.startDirectionTracking(network) },
|
||||
onStopTracking = { viewModel.stopDirectionTracking() },
|
||||
cardinalDirection = viewModel.getCardinalDirection()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
app/src/main/java/com/manalejandro/wifiattack/data/model/AttackEvent.kt
Archivo normal
@@ -0,0 +1,47 @@
|
||||
package com.manalejandro.wifiattack.data.model
|
||||
|
||||
/**
|
||||
* Represents an attack detection event.
|
||||
*
|
||||
* @property id Unique identifier for this attack
|
||||
* @property attackType Type of attack detected
|
||||
* @property targetBssid BSSID of the target network (if known)
|
||||
* @property targetSsid SSID of the target network (if known)
|
||||
* @property channel Channel where the attack was detected
|
||||
* @property estimatedDirection Estimated direction of the attacker in degrees (0-360)
|
||||
* @property signalStrength Signal strength of the attack
|
||||
* @property confidence Confidence level of the detection (0-100)
|
||||
* @property timestamp When the attack was detected
|
||||
* @property isActive Whether the attack is still ongoing
|
||||
*/
|
||||
data class AttackEvent(
|
||||
val id: String = java.util.UUID.randomUUID().toString(),
|
||||
val attackType: AttackType,
|
||||
val targetBssid: String? = null,
|
||||
val targetSsid: String? = null,
|
||||
val channel: Int,
|
||||
val estimatedDirection: Float? = null,
|
||||
val signalStrength: Int,
|
||||
val confidence: Int,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val isActive: Boolean = true
|
||||
) {
|
||||
/**
|
||||
* Duration since the attack was first detected
|
||||
*/
|
||||
val durationMs: Long
|
||||
get() = System.currentTimeMillis() - timestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing types of WiFi attacks
|
||||
*/
|
||||
enum class AttackType(val displayName: String, val description: String) {
|
||||
DEAUTH("Deauthentication", "Forcing devices to disconnect from the network"),
|
||||
DISASSOC("Disassociation", "Terminating client associations with access points"),
|
||||
EVIL_TWIN("Evil Twin", "Fake access point mimicking a legitimate network"),
|
||||
BEACON_FLOOD("Beacon Flood", "Flooding the area with fake access point beacons"),
|
||||
PROBE_FLOOD("Probe Flood", "Excessive probe requests from a single source"),
|
||||
UNKNOWN("Unknown", "Unidentified suspicious activity")
|
||||
}
|
||||
|
||||
52
app/src/main/java/com/manalejandro/wifiattack/data/model/ChannelStats.kt
Archivo normal
@@ -0,0 +1,52 @@
|
||||
package com.manalejandro.wifiattack.data.model
|
||||
|
||||
/**
|
||||
* Represents statistics for a specific WiFi channel.
|
||||
*
|
||||
* @property channel The channel number
|
||||
* @property band The frequency band
|
||||
* @property networksCount Number of networks on this channel
|
||||
* @property averageRssi Average signal strength on this channel
|
||||
* @property suspiciousActivityScore Score indicating potential attack activity (0-100)
|
||||
* @property deauthPacketCount Estimated deauthentication packet count based on anomalies
|
||||
* @property lastUpdateTime Last time this channel was updated
|
||||
*/
|
||||
data class ChannelStats(
|
||||
val channel: Int,
|
||||
val band: WifiBand,
|
||||
val networksCount: Int = 0,
|
||||
val averageRssi: Int = -100,
|
||||
val suspiciousActivityScore: Int = 0,
|
||||
val deauthPacketCount: Int = 0,
|
||||
val lastUpdateTime: Long = System.currentTimeMillis()
|
||||
) {
|
||||
/**
|
||||
* Returns the threat level based on suspicious activity score
|
||||
*/
|
||||
val threatLevel: ThreatLevel
|
||||
get() = when {
|
||||
suspiciousActivityScore >= 80 -> ThreatLevel.CRITICAL
|
||||
suspiciousActivityScore >= 60 -> ThreatLevel.HIGH
|
||||
suspiciousActivityScore >= 40 -> ThreatLevel.MEDIUM
|
||||
suspiciousActivityScore >= 20 -> ThreatLevel.LOW
|
||||
else -> ThreatLevel.NONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this channel has suspicious activity
|
||||
*/
|
||||
val hasSuspiciousActivity: Boolean
|
||||
get() = suspiciousActivityScore >= 40
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing threat levels
|
||||
*/
|
||||
enum class ThreatLevel(val displayName: String, val colorValue: Long) {
|
||||
NONE("None", 0xFF4CAF50), // Green
|
||||
LOW("Low", 0xFF8BC34A), // Light Green
|
||||
MEDIUM("Medium", 0xFFFF9800), // Orange
|
||||
HIGH("High", 0xFFFF5722), // Deep Orange
|
||||
CRITICAL("Critical", 0xFFF44336) // Red
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.manalejandro.wifiattack.data.model
|
||||
|
||||
/**
|
||||
* Represents information about a detected WiFi network.
|
||||
*
|
||||
* @property ssid The network SSID (name)
|
||||
* @property bssid The network BSSID (MAC address)
|
||||
* @property rssi Signal strength in dBm
|
||||
* @property frequency Frequency in MHz
|
||||
* @property channel WiFi channel number
|
||||
* @property capabilities Security capabilities string
|
||||
* @property timestamp Time when this network was detected
|
||||
*/
|
||||
data class WifiNetworkInfo(
|
||||
val ssid: String,
|
||||
val bssid: String,
|
||||
val rssi: Int,
|
||||
val frequency: Int,
|
||||
val channel: Int,
|
||||
val capabilities: String,
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
) {
|
||||
/**
|
||||
* Returns the WiFi band (2.4GHz, 5GHz, or 6GHz)
|
||||
*/
|
||||
val band: WifiBand
|
||||
get() = when {
|
||||
frequency in 2400..2500 -> WifiBand.BAND_2_4GHz
|
||||
frequency in 5150..5875 -> WifiBand.BAND_5GHz
|
||||
frequency in 5925..7125 -> WifiBand.BAND_6GHz
|
||||
else -> WifiBand.UNKNOWN
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns signal strength as a percentage (0-100)
|
||||
*/
|
||||
val signalStrengthPercent: Int
|
||||
get() = when {
|
||||
rssi >= -50 -> 100
|
||||
rssi >= -60 -> 80
|
||||
rssi >= -70 -> 60
|
||||
rssi >= -80 -> 40
|
||||
rssi >= -90 -> 20
|
||||
else -> 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Converts frequency to channel number
|
||||
*/
|
||||
fun frequencyToChannel(frequency: Int): Int {
|
||||
return when {
|
||||
frequency in 2412..2484 -> (frequency - 2412) / 5 + 1
|
||||
frequency in 5170..5825 -> (frequency - 5170) / 5 + 34
|
||||
frequency in 5955..7115 -> (frequency - 5955) / 5 + 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing WiFi frequency bands
|
||||
*/
|
||||
enum class WifiBand(val displayName: String) {
|
||||
BAND_2_4GHz("2.4 GHz"),
|
||||
BAND_5GHz("5 GHz"),
|
||||
BAND_6GHz("6 GHz"),
|
||||
UNKNOWN("Unknown")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.manalejandro.wifiattack.presentation
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.manalejandro.wifiattack.data.model.AttackEvent
|
||||
import com.manalejandro.wifiattack.data.model.ChannelStats
|
||||
import com.manalejandro.wifiattack.data.model.WifiNetworkInfo
|
||||
import com.manalejandro.wifiattack.service.DirectionSensorManager
|
||||
import com.manalejandro.wifiattack.service.WifiScannerService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Main ViewModel for the WiFi Attack Detector application.
|
||||
* Manages WiFi scanning, attack detection, and signal direction tracking.
|
||||
*/
|
||||
class WifiAttackViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val wifiScanner = WifiScannerService(application)
|
||||
private val directionSensor = DirectionSensorManager(application)
|
||||
|
||||
// WiFi Scanner states
|
||||
val networks: StateFlow<List<WifiNetworkInfo>> = wifiScanner.networks
|
||||
val channelStats: StateFlow<List<ChannelStats>> = wifiScanner.channelStats
|
||||
val attacks: StateFlow<List<AttackEvent>> = wifiScanner.attacks
|
||||
val isScanning: StateFlow<Boolean> = wifiScanner.isScanning
|
||||
val lastScanTime: StateFlow<Long> = wifiScanner.lastScanTime
|
||||
|
||||
// Direction sensor states
|
||||
val azimuth: StateFlow<Float> = directionSensor.azimuth
|
||||
val signalDirection: StateFlow<Float?> = directionSensor.signalDirection
|
||||
val signalStrengthAtDirection: StateFlow<Map<Float, Int>> = directionSensor.signalStrengthAtDirection
|
||||
val isSensorAvailable: StateFlow<Boolean> = directionSensor.isAvailable
|
||||
|
||||
// UI State
|
||||
private val _selectedTab = MutableStateFlow(0)
|
||||
val selectedTab: StateFlow<Int> = _selectedTab.asStateFlow()
|
||||
|
||||
private val _selectedNetwork = MutableStateFlow<WifiNetworkInfo?>(null)
|
||||
val selectedNetwork: StateFlow<WifiNetworkInfo?> = _selectedNetwork.asStateFlow()
|
||||
|
||||
private val _isTrackingDirection = MutableStateFlow(false)
|
||||
val isTrackingDirection: StateFlow<Boolean> = _isTrackingDirection.asStateFlow()
|
||||
|
||||
private val _permissionsGranted = MutableStateFlow(false)
|
||||
val permissionsGranted: StateFlow<Boolean> = _permissionsGranted.asStateFlow()
|
||||
|
||||
// Computed states
|
||||
val activeAttacksCount: StateFlow<Int> = attacks
|
||||
.combine(MutableStateFlow(Unit)) { attacks, _ ->
|
||||
attacks.count { it.isActive }
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
|
||||
|
||||
val highestThreatLevel: StateFlow<String> = channelStats
|
||||
.combine(MutableStateFlow(Unit)) { stats, _ ->
|
||||
stats.maxByOrNull { it.suspiciousActivityScore }?.threatLevel?.displayName ?: "None"
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "None")
|
||||
|
||||
/**
|
||||
* Starts WiFi scanning and sensor monitoring.
|
||||
*/
|
||||
fun startMonitoring() {
|
||||
if (!_permissionsGranted.value) return
|
||||
|
||||
wifiScanner.startScanning()
|
||||
directionSensor.startListening()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all monitoring activities.
|
||||
*/
|
||||
fun stopMonitoring() {
|
||||
wifiScanner.stopScanning()
|
||||
directionSensor.stopListening()
|
||||
_isTrackingDirection.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the scanning state.
|
||||
*/
|
||||
fun toggleScanning() {
|
||||
if (isScanning.value) {
|
||||
stopMonitoring()
|
||||
} else {
|
||||
startMonitoring()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the permissions granted state.
|
||||
*/
|
||||
fun setPermissionsGranted(granted: Boolean) {
|
||||
_permissionsGranted.value = granted
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a network for detailed view or tracking.
|
||||
*/
|
||||
fun selectNetwork(network: WifiNetworkInfo?) {
|
||||
_selectedNetwork.value = network
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts tracking the direction of a specific network's signal.
|
||||
*/
|
||||
fun startDirectionTracking(network: WifiNetworkInfo) {
|
||||
_selectedNetwork.value = network
|
||||
_isTrackingDirection.value = true
|
||||
directionSensor.clearReadings()
|
||||
|
||||
// Start recording signal readings for this network
|
||||
viewModelScope.launch {
|
||||
networks.collect { currentNetworks ->
|
||||
if (_isTrackingDirection.value) {
|
||||
val trackedNetwork = currentNetworks.find { it.bssid == network.bssid }
|
||||
trackedNetwork?.let {
|
||||
directionSensor.recordSignalReading(it.rssi, it.bssid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops direction tracking.
|
||||
*/
|
||||
fun stopDirectionTracking() {
|
||||
_isTrackingDirection.value = false
|
||||
_selectedNetwork.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the estimated direction of the selected network.
|
||||
*/
|
||||
fun getEstimatedDirection(): Float? {
|
||||
val network = _selectedNetwork.value ?: return null
|
||||
return directionSensor.calculateSignalDirection(network.bssid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current compass direction as a cardinal direction.
|
||||
*/
|
||||
fun getCardinalDirection(): String = directionSensor.getCardinalDirection()
|
||||
|
||||
/**
|
||||
* Changes the selected tab.
|
||||
*/
|
||||
fun selectTab(index: Int) {
|
||||
_selectedTab.value = index
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all attack history.
|
||||
*/
|
||||
fun clearAttackHistory() {
|
||||
wifiScanner.clearData()
|
||||
directionSensor.clearReadings()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns WiFi enabled status.
|
||||
*/
|
||||
fun isWifiEnabled(): Boolean = wifiScanner.isWifiEnabled()
|
||||
|
||||
/**
|
||||
* Cleans up resources when the ViewModel is cleared.
|
||||
*/
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
stopMonitoring()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
package com.manalejandro.wifiattack.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Place
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.manalejandro.wifiattack.data.model.AttackEvent
|
||||
import com.manalejandro.wifiattack.data.model.AttackType
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Screen displaying attack history and details.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AttacksScreen(
|
||||
attacks: List<AttackEvent>,
|
||||
onClearHistory: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var showClearDialog by remember { mutableStateOf(false) }
|
||||
var selectedAttackType by remember { mutableStateOf<AttackType?>(null) }
|
||||
|
||||
val filteredAttacks = attacks
|
||||
.filter { selectedAttackType == null || it.attackType == selectedAttackType }
|
||||
.sortedByDescending { it.timestamp }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Attack History",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
if (attacks.isNotEmpty()) {
|
||||
IconButton(onClick = { showClearDialog = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Clear history"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Statistics Card
|
||||
if (attacks.isNotEmpty()) {
|
||||
AttackStatisticsCard(attacks = attacks)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
// Filter Chips
|
||||
if (attacks.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selectedAttackType == null,
|
||||
onClick = { selectedAttackType = null },
|
||||
label = { Text("All") }
|
||||
)
|
||||
|
||||
AttackType.entries.take(3).forEach { type ->
|
||||
val count = attacks.count { it.attackType == type }
|
||||
if (count > 0) {
|
||||
FilterChip(
|
||||
selected = selectedAttackType == type,
|
||||
onClick = {
|
||||
selectedAttackType = if (selectedAttackType == type) null else type
|
||||
},
|
||||
label = { Text("${type.displayName} ($count)") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
// Attacks List
|
||||
if (filteredAttacks.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = Color(0xFF4CAF50)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = if (attacks.isEmpty()) "No attacks detected" else "No attacks match filter",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (attacks.isEmpty()) {
|
||||
Text(
|
||||
text = "Your network appears to be safe",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(filteredAttacks) { attack ->
|
||||
AttackDetailCard(attack = attack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Confirmation Dialog
|
||||
if (showClearDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showClearDialog = false },
|
||||
icon = { Icon(Icons.Default.Warning, contentDescription = null) },
|
||||
title = { Text("Clear Attack History") },
|
||||
text = { Text("Are you sure you want to clear all attack history? This action cannot be undone.") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onClearHistory()
|
||||
showClearDialog = false
|
||||
}
|
||||
) {
|
||||
Text("Clear")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showClearDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AttackStatisticsCard(
|
||||
attacks: List<AttackEvent>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val totalAttacks = attacks.size
|
||||
val activeAttacks = attacks.count { it.isActive }
|
||||
val criticalAttacks = attacks.count { it.confidence >= 80 }
|
||||
val uniqueChannels = attacks.map { it.channel }.distinct().size
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Attack Summary",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
StatisticItem(
|
||||
value = totalAttacks.toString(),
|
||||
label = "Total",
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
StatisticItem(
|
||||
value = activeAttacks.toString(),
|
||||
label = "Active",
|
||||
color = Color(0xFFFF9800)
|
||||
)
|
||||
StatisticItem(
|
||||
value = criticalAttacks.toString(),
|
||||
label = "Critical",
|
||||
color = Color(0xFFF44336)
|
||||
)
|
||||
StatisticItem(
|
||||
value = uniqueChannels.toString(),
|
||||
label = "Channels",
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Attack Type Breakdown
|
||||
val attackTypeCounts = attacks.groupBy { it.attackType }
|
||||
.mapValues { it.value.size }
|
||||
.entries
|
||||
.sortedByDescending { it.value }
|
||||
|
||||
Text(
|
||||
text = "Attack Types:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
attackTypeCounts.take(3).forEach { (type, count) ->
|
||||
AssistChip(
|
||||
onClick = { },
|
||||
label = { Text("${type.displayName}: $count") },
|
||||
modifier = Modifier.height(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatisticItem(
|
||||
value: String,
|
||||
label: String,
|
||||
color: Color,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = color
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AttackDetailCard(
|
||||
attack: AttackEvent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val threatColor = when {
|
||||
attack.confidence >= 80 -> Color(0xFFF44336)
|
||||
attack.confidence >= 60 -> Color(0xFFFF5722)
|
||||
attack.confidence >= 40 -> Color(0xFFFF9800)
|
||||
else -> Color(0xFF8BC34A)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
// Header Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (attack.attackType) {
|
||||
AttackType.DEAUTH -> Icons.Default.Close
|
||||
AttackType.EVIL_TWIN -> Icons.Default.Warning
|
||||
AttackType.BEACON_FLOOD -> Icons.Default.Info
|
||||
AttackType.PROBE_FLOOD -> Icons.Default.Search
|
||||
else -> Icons.Default.Warning
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = threatColor,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = attack.attackType.displayName,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = attack.attackType.description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Confidence Badge
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(threatColor.copy(alpha = 0.2f))
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "${attack.confidence}%",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = threatColor,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Details Grid
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
DetailItem(
|
||||
label = "Target",
|
||||
value = attack.targetSsid ?: attack.targetBssid?.take(17) ?: "Unknown"
|
||||
)
|
||||
DetailItem(
|
||||
label = "Channel",
|
||||
value = attack.channel.toString()
|
||||
)
|
||||
DetailItem(
|
||||
label = "Signal",
|
||||
value = "${attack.signalStrength} dBm"
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Timestamp
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = formatDateTime(attack.timestamp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
// Direction if available
|
||||
attack.estimatedDirection?.let { direction ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Place,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "${direction.toInt()}°",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Status Badge
|
||||
if (attack.isActive) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(Color(0xFFFF9800).copy(alpha = 0.2f))
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "ACTIVE",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color(0xFFFF9800),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailItem(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDateTime(timestamp: Long): String {
|
||||
val sdf = SimpleDateFormat("MMM dd, HH:mm:ss", Locale.getDefault())
|
||||
return sdf.format(Date(timestamp))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
package com.manalejandro.wifiattack.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.manalejandro.wifiattack.data.model.ChannelStats
|
||||
import com.manalejandro.wifiattack.data.model.WifiBand
|
||||
|
||||
/**
|
||||
* Screen displaying detailed channel statistics and error packet counts.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChannelStatsScreen(
|
||||
channelStats: List<ChannelStats>,
|
||||
isScanning: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var selectedBand by remember { mutableStateOf<WifiBand?>(null) }
|
||||
var sortByThreat by remember { mutableStateOf(true) }
|
||||
|
||||
val filteredStats = channelStats
|
||||
.filter { selectedBand == null || it.band == selectedBand }
|
||||
.let { stats ->
|
||||
if (sortByThreat) {
|
||||
stats.sortedByDescending { it.suspiciousActivityScore }
|
||||
} else {
|
||||
stats.sortedBy { it.channel }
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Header with filters
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Channel Statistics",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
if (isScanning) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
text = "Live",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Band Filter Chips
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selectedBand == null,
|
||||
onClick = { selectedBand = null },
|
||||
label = { Text("All") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = selectedBand == WifiBand.BAND_2_4GHz,
|
||||
onClick = { selectedBand = WifiBand.BAND_2_4GHz },
|
||||
label = { Text("2.4 GHz") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = selectedBand == WifiBand.BAND_5GHz,
|
||||
onClick = { selectedBand = WifiBand.BAND_5GHz },
|
||||
label = { Text("5 GHz") }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Sort Toggle
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Sort by: ",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
TextButton(onClick = { sortByThreat = !sortByThreat }) {
|
||||
Text(if (sortByThreat) "Threat Level" else "Channel Number")
|
||||
Icon(
|
||||
imageVector = if (sortByThreat) Icons.Default.Warning else Icons.Default.List,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Stats Summary
|
||||
if (filteredStats.isNotEmpty()) {
|
||||
StatsSummaryCard(stats = filteredStats)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
// Channel List
|
||||
if (filteredStats.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "No channel data available",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "Start monitoring to see channel statistics",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(filteredStats) { stat ->
|
||||
ChannelDetailCard(stat = stat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatsSummaryCard(
|
||||
stats: List<ChannelStats>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val totalNetworks = stats.sumOf { it.networksCount }
|
||||
val totalDeauthPackets = stats.sumOf { it.deauthPacketCount }
|
||||
val suspiciousChannels = stats.count { it.hasSuspiciousActivity }
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
StatItem(
|
||||
value = stats.size.toString(),
|
||||
label = "Channels",
|
||||
icon = Icons.Default.Build
|
||||
)
|
||||
StatItem(
|
||||
value = totalNetworks.toString(),
|
||||
label = "Networks",
|
||||
icon = Icons.Default.Check
|
||||
)
|
||||
StatItem(
|
||||
value = totalDeauthPackets.toString(),
|
||||
label = "Error Pkts",
|
||||
icon = Icons.Default.Info
|
||||
)
|
||||
StatItem(
|
||||
value = suspiciousChannels.toString(),
|
||||
label = "Suspicious",
|
||||
icon = Icons.Default.Warning
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatItem(
|
||||
value: String,
|
||||
label: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelDetailCard(
|
||||
stat: ChannelStats,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val threatColor = Color(stat.threatLevel.colorValue)
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Channel ${stat.channel}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
AssistChip(
|
||||
onClick = { },
|
||||
label = { Text(stat.band.displayName) },
|
||||
modifier = Modifier.height(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Threat Level Badge
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(threatColor.copy(alpha = 0.2f))
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stat.threatLevel.displayName,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = threatColor,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Stats Grid
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "Networks",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${stat.networksCount}",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = "Avg RSSI",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${stat.averageRssi} dBm",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = "Error Packets",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${stat.deauthPacketCount}",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (stat.deauthPacketCount > 20) threatColor else Color.Unspecified
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Suspicious Activity Bar
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Suspicious Activity",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
text = "${stat.suspiciousActivityScore}%",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = threatColor
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { stat.suspiciousActivityScore / 100f },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(4.dp)),
|
||||
color = threatColor,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
package com.manalejandro.wifiattack.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.manalejandro.wifiattack.data.model.AttackEvent
|
||||
import com.manalejandro.wifiattack.data.model.ChannelStats
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Main dashboard screen showing overview of WiFi monitoring status.
|
||||
*/
|
||||
@Composable
|
||||
fun DashboardScreen(
|
||||
isScanning: Boolean,
|
||||
networksCount: Int,
|
||||
activeAttacksCount: Int,
|
||||
highestThreatLevel: String,
|
||||
channelStats: List<ChannelStats>,
|
||||
recentAttacks: List<AttackEvent>,
|
||||
lastScanTime: Long,
|
||||
onToggleScanning: () -> Unit,
|
||||
onNavigateToChannels: () -> Unit,
|
||||
onNavigateToAttacks: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Status Card
|
||||
item {
|
||||
StatusCard(
|
||||
isScanning = isScanning,
|
||||
networksCount = networksCount,
|
||||
lastScanTime = lastScanTime,
|
||||
onToggleScanning = onToggleScanning
|
||||
)
|
||||
}
|
||||
|
||||
// Threat Overview Card
|
||||
item {
|
||||
ThreatOverviewCard(
|
||||
activeAttacksCount = activeAttacksCount,
|
||||
highestThreatLevel = highestThreatLevel,
|
||||
onViewAttacks = onNavigateToAttacks
|
||||
)
|
||||
}
|
||||
|
||||
// Channel Summary Card
|
||||
item {
|
||||
ChannelSummaryCard(
|
||||
channelStats = channelStats,
|
||||
onViewDetails = onNavigateToChannels
|
||||
)
|
||||
}
|
||||
|
||||
// Recent Attacks Section
|
||||
if (recentAttacks.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Recent Attacks",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
items(recentAttacks.take(3)) { attack ->
|
||||
AttackEventCard(attack = attack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusCard(
|
||||
isScanning: Boolean,
|
||||
networksCount: Int,
|
||||
lastScanTime: Long,
|
||||
onToggleScanning: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isScanning)
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = if (isScanning) "Monitoring Active" else "Monitoring Stopped",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "$networksCount networks detected",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
if (lastScanTime > 0) {
|
||||
Text(
|
||||
text = "Last scan: ${formatTime(lastScanTime)}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FilledIconToggleButton(
|
||||
checked = isScanning,
|
||||
onCheckedChange = { onToggleScanning() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isScanning) Icons.Default.Close else Icons.Default.PlayArrow,
|
||||
contentDescription = if (isScanning) "Stop" else "Start"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isScanning) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreatOverviewCard(
|
||||
activeAttacksCount: Int,
|
||||
highestThreatLevel: String,
|
||||
onViewAttacks: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val threatColor = when (highestThreatLevel) {
|
||||
"Critical" -> Color(0xFFF44336)
|
||||
"High" -> Color(0xFFFF5722)
|
||||
"Medium" -> Color(0xFFFF9800)
|
||||
"Low" -> Color(0xFF8BC34A)
|
||||
else -> Color(0xFF4CAF50)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
onClick = onViewAttacks
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(threatColor.copy(alpha = 0.2f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
tint = threatColor,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = "Threat Level: $highestThreatLevel",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = "$activeAttacksCount active attack(s) detected",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowRight,
|
||||
contentDescription = "View details"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelSummaryCard(
|
||||
channelStats: List<ChannelStats>,
|
||||
onViewDetails: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
onClick = onViewDetails
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Channel Activity",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowRight,
|
||||
contentDescription = "View details"
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
if (channelStats.isEmpty()) {
|
||||
Text(
|
||||
text = "No channel data available. Start monitoring to see activity.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
} else {
|
||||
// Show top 5 channels with activity
|
||||
val topChannels = channelStats
|
||||
.sortedByDescending { it.suspiciousActivityScore }
|
||||
.take(5)
|
||||
|
||||
topChannels.forEach { stat ->
|
||||
ChannelActivityBar(stat = stat)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelActivityBar(
|
||||
stat: ChannelStats,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val threatColor = Color(stat.threatLevel.colorValue)
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Ch ${stat.channel} (${stat.band.displayName})",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
text = "${stat.networksCount} networks",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { stat.suspiciousActivityScore / 100f },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(4.dp)),
|
||||
color = threatColor,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AttackEventCard(
|
||||
attack: AttackEvent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val threatColor = when {
|
||||
attack.confidence >= 80 -> Color(0xFFF44336)
|
||||
attack.confidence >= 60 -> Color(0xFFFF5722)
|
||||
attack.confidence >= 40 -> Color(0xFFFF9800)
|
||||
else -> Color(0xFF8BC34A)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = threatColor.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = threatColor,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = attack.attackType.displayName,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = attack.targetSsid ?: attack.targetBssid ?: "Unknown target",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
text = "Channel ${attack.channel} • ${attack.confidence}% confidence",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = formatTime(attack.timestamp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTime(timestamp: Long): String {
|
||||
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||
return sdf.format(Date(timestamp))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
package com.manalejandro.wifiattack.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Place
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.manalejandro.wifiattack.data.model.WifiNetworkInfo
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* Screen for tracking signal direction using device compass.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DirectionScreen(
|
||||
networks: List<WifiNetworkInfo>,
|
||||
azimuth: Float,
|
||||
signalDirection: Float?,
|
||||
signalStrengthAtDirection: Map<Float, Int>,
|
||||
isTrackingDirection: Boolean,
|
||||
selectedNetwork: WifiNetworkInfo?,
|
||||
isSensorAvailable: Boolean,
|
||||
onStartTracking: (WifiNetworkInfo) -> Unit,
|
||||
onStopTracking: () -> Unit,
|
||||
cardinalDirection: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Header
|
||||
Text(
|
||||
text = "Signal Direction Tracker",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (!isSensorAvailable) {
|
||||
// Sensor not available warning
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
Text(
|
||||
text = "Compass sensor not available on this device",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Compass Card
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = if (isTrackingDirection && selectedNetwork != null) {
|
||||
"Tracking: ${selectedNetwork.ssid}"
|
||||
} else {
|
||||
"Select a network to track"
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Compass View
|
||||
CompassView(
|
||||
azimuth = azimuth,
|
||||
signalDirection = signalDirection,
|
||||
signalStrengthAtDirection = signalStrengthAtDirection,
|
||||
isTracking = isTrackingDirection,
|
||||
modifier = Modifier.size(250.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Current Direction Display
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "Device Heading",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${azimuth.toInt()}° $cardinalDirection",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
if (isTrackingDirection && signalDirection != null) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "Signal Direction",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${signalDirection.toInt()}°",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isTrackingDirection) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Slowly rotate your device to find the strongest signal",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedButton(onClick = onStopTracking) {
|
||||
Icon(Icons.Default.Close, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Stop Tracking")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Network Selection List
|
||||
Text(
|
||||
text = "Available Networks",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (networks.isEmpty()) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No networks found. Start scanning to see available networks.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(
|
||||
networks.sortedByDescending { it.rssi }
|
||||
) { network ->
|
||||
NetworkTrackCard(
|
||||
network = network,
|
||||
isSelected = selectedNetwork?.bssid == network.bssid,
|
||||
isTracking = isTrackingDirection && selectedNetwork?.bssid == network.bssid,
|
||||
onTrack = { onStartTracking(network) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompassView(
|
||||
azimuth: Float,
|
||||
signalDirection: Float?,
|
||||
signalStrengthAtDirection: Map<Float, Int>,
|
||||
isTracking: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val surfaceColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
|
||||
val signalColor = Color(0xFF4CAF50)
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val center = Offset(size.width / 2, size.height / 2)
|
||||
val radius = size.minDimension / 2 - 20.dp.toPx()
|
||||
|
||||
// Draw compass circle
|
||||
drawCircle(
|
||||
color = surfaceColor,
|
||||
radius = radius,
|
||||
center = center
|
||||
)
|
||||
|
||||
// Draw compass outline
|
||||
drawCircle(
|
||||
color = onSurfaceColor.copy(alpha = 0.3f),
|
||||
radius = radius,
|
||||
center = center,
|
||||
style = Stroke(width = 2.dp.toPx())
|
||||
)
|
||||
|
||||
// Draw direction markers
|
||||
for (i in 0 until 360 step 30) {
|
||||
val angle = Math.toRadians(i.toDouble() - 90)
|
||||
val isCardinal = i % 90 == 0
|
||||
val lineLength = if (isCardinal) 20.dp.toPx() else 10.dp.toPx()
|
||||
|
||||
val startRadius = radius - lineLength
|
||||
val startX = center.x + (startRadius * cos(angle)).toFloat()
|
||||
val startY = center.y + (startRadius * sin(angle)).toFloat()
|
||||
val endX = center.x + (radius * cos(angle)).toFloat()
|
||||
val endY = center.y + (radius * sin(angle)).toFloat()
|
||||
|
||||
drawLine(
|
||||
color = onSurfaceColor.copy(alpha = if (isCardinal) 0.8f else 0.4f),
|
||||
start = Offset(startX, startY),
|
||||
end = Offset(endX, endY),
|
||||
strokeWidth = if (isCardinal) 3.dp.toPx() else 1.dp.toPx()
|
||||
)
|
||||
}
|
||||
|
||||
// Draw signal strength at each direction if tracking
|
||||
if (isTracking && signalStrengthAtDirection.isNotEmpty()) {
|
||||
signalStrengthAtDirection.forEach { (direction, rssi) ->
|
||||
val normalizedStrength = ((rssi + 100) / 70f).coerceIn(0f, 1f)
|
||||
val signalRadius = radius * 0.3f + (radius * 0.5f * normalizedStrength)
|
||||
val angle = Math.toRadians(direction.toDouble() - 90)
|
||||
|
||||
val x = center.x + (signalRadius * cos(angle)).toFloat()
|
||||
val y = center.y + (signalRadius * sin(angle)).toFloat()
|
||||
|
||||
drawCircle(
|
||||
color = signalColor.copy(alpha = 0.6f),
|
||||
radius = 8.dp.toPx(),
|
||||
center = Offset(x, y)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw signal direction arrow if available
|
||||
signalDirection?.let { direction ->
|
||||
val arrowAngle = Math.toRadians(direction.toDouble() - 90)
|
||||
val arrowRadius = radius * 0.7f
|
||||
|
||||
val arrowX = center.x + (arrowRadius * cos(arrowAngle)).toFloat()
|
||||
val arrowY = center.y + (arrowRadius * sin(arrowAngle)).toFloat()
|
||||
|
||||
drawCircle(
|
||||
color = signalColor,
|
||||
radius = 12.dp.toPx(),
|
||||
center = Offset(arrowX, arrowY)
|
||||
)
|
||||
}
|
||||
|
||||
// Draw device direction indicator (triangle/arrow)
|
||||
rotate(-azimuth, center) {
|
||||
val triangleSize = 15.dp.toPx()
|
||||
val topY = center.y - radius + 5.dp.toPx()
|
||||
|
||||
val path = androidx.compose.ui.graphics.Path().apply {
|
||||
moveTo(center.x, topY)
|
||||
lineTo(center.x - triangleSize / 2, topY + triangleSize)
|
||||
lineTo(center.x + triangleSize / 2, topY + triangleSize)
|
||||
close()
|
||||
}
|
||||
|
||||
drawPath(
|
||||
path = path,
|
||||
color = primaryColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Cardinal Direction Labels
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "N",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.offset(y = (-100).dp)
|
||||
)
|
||||
Text(
|
||||
text = "S",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.offset(y = 100.dp)
|
||||
)
|
||||
Text(
|
||||
text = "E",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.offset(x = 100.dp)
|
||||
)
|
||||
Text(
|
||||
text = "W",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.offset(x = (-100).dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkTrackCard(
|
||||
network: WifiNetworkInfo,
|
||||
isSelected: Boolean,
|
||||
isTracking: Boolean,
|
||||
onTrack: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected)
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.surface
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Signal Strength Indicator
|
||||
SignalStrengthIcon(rssi = network.rssi)
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = network.ssid,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
text = "${network.rssi} dBm • Ch ${network.channel}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isTracking) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = onTrack) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Place,
|
||||
contentDescription = "Track",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SignalStrengthIcon(
|
||||
rssi: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val signalLevel = when {
|
||||
rssi >= -50 -> 4
|
||||
rssi >= -60 -> 3
|
||||
rssi >= -70 -> 2
|
||||
rssi >= -80 -> 1
|
||||
else -> 0
|
||||
}
|
||||
|
||||
val signalColor = when (signalLevel) {
|
||||
4 -> Color(0xFF4CAF50)
|
||||
3 -> Color(0xFF8BC34A)
|
||||
2 -> Color(0xFFFF9800)
|
||||
1 -> Color(0xFFFF5722)
|
||||
else -> Color(0xFFF44336)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Signal strength",
|
||||
modifier = modifier.size(24.dp),
|
||||
tint = signalColor
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
package com.manalejandro.wifiattack.service
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* Manages device orientation sensors for tracking signal direction.
|
||||
* Uses accelerometer and magnetometer to determine compass heading.
|
||||
*/
|
||||
class DirectionSensorManager(context: Context) : SensorEventListener {
|
||||
|
||||
private val sensorManager: SensorManager by lazy {
|
||||
context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
}
|
||||
|
||||
private val accelerometer: Sensor? by lazy {
|
||||
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
}
|
||||
|
||||
private val magnetometer: Sensor? by lazy {
|
||||
sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
||||
}
|
||||
|
||||
private val _azimuth = MutableStateFlow(0f)
|
||||
val azimuth: StateFlow<Float> = _azimuth.asStateFlow()
|
||||
|
||||
private val _pitch = MutableStateFlow(0f)
|
||||
val pitch: StateFlow<Float> = _pitch.asStateFlow()
|
||||
|
||||
private val _roll = MutableStateFlow(0f)
|
||||
val roll: StateFlow<Float> = _roll.asStateFlow()
|
||||
|
||||
private val _isAvailable = MutableStateFlow(false)
|
||||
val isAvailable: StateFlow<Boolean> = _isAvailable.asStateFlow()
|
||||
|
||||
// Signal direction tracking
|
||||
private val _signalDirection = MutableStateFlow<Float?>(null)
|
||||
val signalDirection: StateFlow<Float?> = _signalDirection.asStateFlow()
|
||||
|
||||
private val _signalStrengthAtDirection = MutableStateFlow<Map<Float, Int>>(emptyMap())
|
||||
val signalStrengthAtDirection: StateFlow<Map<Float, Int>> = _signalStrengthAtDirection.asStateFlow()
|
||||
|
||||
private var lastAccelerometerValues: FloatArray? = null
|
||||
private var lastMagnetometerValues: FloatArray? = null
|
||||
|
||||
private val rotationMatrix = FloatArray(9)
|
||||
private val orientationAngles = FloatArray(3)
|
||||
|
||||
// For signal tracking
|
||||
private val directionReadings = mutableListOf<DirectionReading>()
|
||||
private val directionHistoryWindow = 30_000L // 30 seconds
|
||||
|
||||
/**
|
||||
* Starts listening to orientation sensors.
|
||||
*/
|
||||
fun startListening() {
|
||||
val hasAccelerometer = accelerometer != null
|
||||
val hasMagnetometer = magnetometer != null
|
||||
|
||||
_isAvailable.value = hasAccelerometer && hasMagnetometer
|
||||
|
||||
if (hasAccelerometer) {
|
||||
sensorManager.registerListener(
|
||||
this,
|
||||
accelerometer,
|
||||
SensorManager.SENSOR_DELAY_UI
|
||||
)
|
||||
}
|
||||
|
||||
if (hasMagnetometer) {
|
||||
sensorManager.registerListener(
|
||||
this,
|
||||
magnetometer,
|
||||
SensorManager.SENSOR_DELAY_UI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening to sensors.
|
||||
*/
|
||||
fun stopListening() {
|
||||
sensorManager.unregisterListener(this)
|
||||
lastAccelerometerValues = null
|
||||
lastMagnetometerValues = null
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent?) {
|
||||
event ?: return
|
||||
|
||||
when (event.sensor.type) {
|
||||
Sensor.TYPE_ACCELEROMETER -> {
|
||||
lastAccelerometerValues = event.values.clone()
|
||||
}
|
||||
Sensor.TYPE_MAGNETIC_FIELD -> {
|
||||
lastMagnetometerValues = event.values.clone()
|
||||
}
|
||||
}
|
||||
|
||||
updateOrientation()
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
|
||||
// Not needed for this implementation
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates orientation values from sensor data.
|
||||
*/
|
||||
private fun updateOrientation() {
|
||||
val accelerometerValues = lastAccelerometerValues ?: return
|
||||
val magnetometerValues = lastMagnetometerValues ?: return
|
||||
|
||||
val success = SensorManager.getRotationMatrix(
|
||||
rotationMatrix,
|
||||
null,
|
||||
accelerometerValues,
|
||||
magnetometerValues
|
||||
)
|
||||
|
||||
if (success) {
|
||||
SensorManager.getOrientation(rotationMatrix, orientationAngles)
|
||||
|
||||
// Convert to degrees
|
||||
val azimuthDegrees = Math.toDegrees(orientationAngles[0].toDouble()).toFloat()
|
||||
val pitchDegrees = Math.toDegrees(orientationAngles[1].toDouble()).toFloat()
|
||||
val rollDegrees = Math.toDegrees(orientationAngles[2].toDouble()).toFloat()
|
||||
|
||||
// Normalize azimuth to 0-360
|
||||
_azimuth.value = (azimuthDegrees + 360) % 360
|
||||
_pitch.value = pitchDegrees
|
||||
_roll.value = rollDegrees
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a signal reading at the current direction.
|
||||
* @param rssi Signal strength in dBm
|
||||
* @param bssid BSSID of the network being tracked
|
||||
*/
|
||||
fun recordSignalReading(rssi: Int, bssid: String) {
|
||||
val currentDirection = _azimuth.value
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
directionReadings.add(
|
||||
DirectionReading(
|
||||
direction = currentDirection,
|
||||
rssi = rssi,
|
||||
bssid = bssid,
|
||||
timestamp = currentTime
|
||||
)
|
||||
)
|
||||
|
||||
// Remove old readings
|
||||
directionReadings.removeAll { currentTime - it.timestamp > directionHistoryWindow }
|
||||
|
||||
// Update signal strength map
|
||||
updateSignalStrengthMap(bssid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the signal strength map for direction analysis.
|
||||
*/
|
||||
private fun updateSignalStrengthMap(bssid: String) {
|
||||
val relevantReadings = directionReadings.filter { it.bssid == bssid }
|
||||
|
||||
if (relevantReadings.isEmpty()) return
|
||||
|
||||
// Group readings by direction buckets (every 10 degrees)
|
||||
val directionBuckets = relevantReadings.groupBy { reading ->
|
||||
((reading.direction / 10).toInt() * 10).toFloat()
|
||||
}.mapValues { (_, readings) ->
|
||||
readings.map { it.rssi }.average().toInt()
|
||||
}
|
||||
|
||||
_signalStrengthAtDirection.value = directionBuckets
|
||||
|
||||
// Find direction with strongest signal
|
||||
val strongestDirection = directionBuckets.maxByOrNull { it.value }
|
||||
_signalDirection.value = strongestDirection?.key
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the estimated direction of a signal source.
|
||||
* Uses collected readings to triangulate the strongest signal direction.
|
||||
* @param bssid The BSSID to track
|
||||
* @return Estimated direction in degrees (0-360), or null if insufficient data
|
||||
*/
|
||||
fun calculateSignalDirection(bssid: String): Float? {
|
||||
val readings = directionReadings.filter { it.bssid == bssid }
|
||||
|
||||
if (readings.size < 4) return null // Need multiple readings
|
||||
|
||||
// Find the direction range with consistently strongest signal
|
||||
val directionBuckets = readings.groupBy { reading ->
|
||||
((reading.direction / 30).toInt() * 30).toFloat()
|
||||
}
|
||||
|
||||
val strongestBucket = directionBuckets.maxByOrNull { (_, bucketReadings) ->
|
||||
bucketReadings.map { it.rssi }.average()
|
||||
}
|
||||
|
||||
return strongestBucket?.key
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all recorded signal readings.
|
||||
*/
|
||||
fun clearReadings() {
|
||||
directionReadings.clear()
|
||||
_signalStrengthAtDirection.value = emptyMap()
|
||||
_signalDirection.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the compass heading as a cardinal direction.
|
||||
*/
|
||||
fun getCardinalDirection(): String {
|
||||
val azimuth = _azimuth.value
|
||||
return when {
|
||||
azimuth >= 337.5 || azimuth < 22.5 -> "N"
|
||||
azimuth >= 22.5 && azimuth < 67.5 -> "NE"
|
||||
azimuth >= 67.5 && azimuth < 112.5 -> "E"
|
||||
azimuth >= 112.5 && azimuth < 157.5 -> "SE"
|
||||
azimuth >= 157.5 && azimuth < 202.5 -> "S"
|
||||
azimuth >= 202.5 && azimuth < 247.5 -> "SW"
|
||||
azimuth >= 247.5 && azimuth < 292.5 -> "W"
|
||||
azimuth >= 292.5 && azimuth < 337.5 -> "NW"
|
||||
else -> "N"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class representing a signal reading at a specific direction.
|
||||
*/
|
||||
data class DirectionReading(
|
||||
val direction: Float,
|
||||
val rssi: Int,
|
||||
val bssid: String,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
376
app/src/main/java/com/manalejandro/wifiattack/service/WifiScannerService.kt
Archivo normal
@@ -0,0 +1,376 @@
|
||||
package com.manalejandro.wifiattack.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.wifi.ScanResult
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import com.manalejandro.wifiattack.data.model.AttackEvent
|
||||
import com.manalejandro.wifiattack.data.model.AttackType
|
||||
import com.manalejandro.wifiattack.data.model.ChannelStats
|
||||
import com.manalejandro.wifiattack.data.model.WifiBand
|
||||
import com.manalejandro.wifiattack.data.model.WifiNetworkInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Service responsible for scanning WiFi networks and detecting attacks.
|
||||
* Uses WifiManager to perform scans and analyzes results for suspicious patterns.
|
||||
*/
|
||||
class WifiScannerService(private val context: Context) {
|
||||
|
||||
private val wifiManager: WifiManager by lazy {
|
||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
}
|
||||
|
||||
private val _networks = MutableStateFlow<List<WifiNetworkInfo>>(emptyList())
|
||||
val networks: StateFlow<List<WifiNetworkInfo>> = _networks.asStateFlow()
|
||||
|
||||
private val _channelStats = MutableStateFlow<List<ChannelStats>>(emptyList())
|
||||
val channelStats: StateFlow<List<ChannelStats>> = _channelStats.asStateFlow()
|
||||
|
||||
private val _attacks = MutableStateFlow<List<AttackEvent>>(emptyList())
|
||||
val attacks: StateFlow<List<AttackEvent>> = _attacks.asStateFlow()
|
||||
|
||||
private val _isScanning = MutableStateFlow(false)
|
||||
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
||||
|
||||
private val _lastScanTime = MutableStateFlow(0L)
|
||||
val lastScanTime: StateFlow<Long> = _lastScanTime.asStateFlow()
|
||||
|
||||
private var scanJob: Job? = null
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
// History for anomaly detection
|
||||
private val networkHistory = mutableMapOf<String, MutableList<WifiNetworkInfo>>()
|
||||
private val channelHistory = mutableMapOf<Int, MutableList<Int>>() // channel -> network counts
|
||||
|
||||
private val wifiReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) {
|
||||
val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)
|
||||
processScanResults(success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts continuous WiFi scanning.
|
||||
* @param intervalMs Interval between scans in milliseconds
|
||||
*/
|
||||
fun startScanning(intervalMs: Long = 5000) {
|
||||
if (_isScanning.value) return
|
||||
|
||||
_isScanning.value = true
|
||||
registerReceiver()
|
||||
|
||||
scanJob = scope.launch {
|
||||
while (_isScanning.value) {
|
||||
performScan()
|
||||
delay(intervalMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the scanning process.
|
||||
*/
|
||||
fun stopScanning() {
|
||||
_isScanning.value = false
|
||||
scanJob?.cancel()
|
||||
scanJob = null
|
||||
unregisterReceiver()
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a single WiFi scan.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
fun performScan() {
|
||||
if (!wifiManager.isWifiEnabled) {
|
||||
return
|
||||
}
|
||||
wifiManager.startScan()
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the broadcast receiver for scan results.
|
||||
*/
|
||||
private fun registerReceiver() {
|
||||
val filter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.registerReceiver(wifiReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
context.registerReceiver(wifiReceiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the broadcast receiver.
|
||||
*/
|
||||
private fun unregisterReceiver() {
|
||||
try {
|
||||
context.unregisterReceiver(wifiReceiver)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Receiver not registered
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the scan results and updates states.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private fun processScanResults(success: Boolean) {
|
||||
if (!success) return
|
||||
|
||||
val scanResults = try {
|
||||
wifiManager.scanResults
|
||||
} catch (e: SecurityException) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
_lastScanTime.value = System.currentTimeMillis()
|
||||
|
||||
val networkInfoList = scanResults.map { result ->
|
||||
WifiNetworkInfo(
|
||||
ssid = getSsid(result),
|
||||
bssid = result.BSSID ?: "",
|
||||
rssi = result.level,
|
||||
frequency = result.frequency,
|
||||
channel = WifiNetworkInfo.frequencyToChannel(result.frequency),
|
||||
capabilities = result.capabilities ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
_networks.value = networkInfoList
|
||||
|
||||
// Update history
|
||||
updateNetworkHistory(networkInfoList)
|
||||
|
||||
// Calculate channel statistics
|
||||
val stats = calculateChannelStats(networkInfoList)
|
||||
_channelStats.value = stats
|
||||
|
||||
// Detect attacks
|
||||
val detectedAttacks = detectAttacks(networkInfoList, stats)
|
||||
if (detectedAttacks.isNotEmpty()) {
|
||||
val currentAttacks = _attacks.value.toMutableList()
|
||||
currentAttacks.addAll(detectedAttacks)
|
||||
// Keep only recent attacks (last 100)
|
||||
_attacks.value = currentAttacks.takeLast(100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SSID from a scan result handling hidden networks.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getSsid(result: ScanResult): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
result.wifiSsid?.toString()?.removeSurrounding("\"") ?: "<Hidden>"
|
||||
} else {
|
||||
result.SSID?.takeIf { it.isNotEmpty() } ?: "<Hidden>"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the network history for anomaly detection.
|
||||
*/
|
||||
private fun updateNetworkHistory(networks: List<WifiNetworkInfo>) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val historyWindow = 60_000L // Keep 60 seconds of history
|
||||
|
||||
networks.forEach { network ->
|
||||
val history = networkHistory.getOrPut(network.bssid) { mutableListOf() }
|
||||
history.add(network)
|
||||
// Remove old entries
|
||||
history.removeAll { currentTime - it.timestamp > historyWindow }
|
||||
}
|
||||
|
||||
// Clean up networks no longer visible
|
||||
val currentBssids = networks.map { it.bssid }.toSet()
|
||||
networkHistory.keys.toList().forEach { bssid ->
|
||||
if (bssid !in currentBssids) {
|
||||
val history = networkHistory[bssid]
|
||||
if (history != null) {
|
||||
history.removeAll { currentTime - it.timestamp > historyWindow }
|
||||
if (history.isEmpty()) {
|
||||
networkHistory.remove(bssid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates statistics for each channel.
|
||||
*/
|
||||
private fun calculateChannelStats(networks: List<WifiNetworkInfo>): List<ChannelStats> {
|
||||
val channelGroups = networks.groupBy { it.channel }
|
||||
|
||||
return channelGroups.map { (channel, channelNetworks) ->
|
||||
val count = channelNetworks.size
|
||||
val avgRssi = channelNetworks.map { it.rssi }.average().toInt()
|
||||
val band = channelNetworks.firstOrNull()?.band ?: WifiBand.UNKNOWN
|
||||
|
||||
// Update channel history
|
||||
val history = channelHistory.getOrPut(channel) { mutableListOf() }
|
||||
history.add(count)
|
||||
if (history.size > 12) { // Keep last 12 scans (~1 minute at 5s interval)
|
||||
history.removeAt(0)
|
||||
}
|
||||
|
||||
// Calculate suspicious activity score
|
||||
val suspiciousScore = calculateSuspiciousScore(channel, channelNetworks)
|
||||
val deauthCount = estimateDeauthPackets(channel, channelNetworks)
|
||||
|
||||
ChannelStats(
|
||||
channel = channel,
|
||||
band = band,
|
||||
networksCount = count,
|
||||
averageRssi = avgRssi,
|
||||
suspiciousActivityScore = suspiciousScore,
|
||||
deauthPacketCount = deauthCount
|
||||
)
|
||||
}.sortedBy { it.channel }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a suspicious activity score for a channel.
|
||||
*/
|
||||
private fun calculateSuspiciousScore(channel: Int, networks: List<WifiNetworkInfo>): Int {
|
||||
var score = 0
|
||||
|
||||
// Check for sudden network count changes
|
||||
val history = channelHistory[channel] ?: return 0
|
||||
if (history.size >= 2) {
|
||||
val currentCount = networks.size
|
||||
val previousCount = history[history.size - 2]
|
||||
val variance = kotlin.math.abs(currentCount - previousCount)
|
||||
|
||||
// Large sudden changes indicate potential beacon flood or deauth
|
||||
if (variance >= 5) score += 30
|
||||
else if (variance >= 3) score += 15
|
||||
}
|
||||
|
||||
// Check for RSSI fluctuations (potential jamming)
|
||||
networks.forEach { network ->
|
||||
val networkHist = networkHistory[network.bssid] ?: return@forEach
|
||||
if (networkHist.size >= 2) {
|
||||
val rssiVariance = networkHist.takeLast(5).map { it.rssi }
|
||||
.zipWithNext { a, b -> kotlin.math.abs(a - b) }
|
||||
.maxOrNull() ?: 0
|
||||
|
||||
if (rssiVariance >= 20) score += 20
|
||||
else if (rssiVariance >= 10) score += 10
|
||||
}
|
||||
}
|
||||
|
||||
// Check for suspicious network names (Evil Twin indicators)
|
||||
val ssidGroups = networks.groupBy { it.ssid.lowercase() }
|
||||
ssidGroups.forEach { (_, similarNetworks) ->
|
||||
if (similarNetworks.size > 1) {
|
||||
// Multiple networks with same SSID on same channel - potential evil twin
|
||||
score += similarNetworks.size * 10
|
||||
}
|
||||
}
|
||||
|
||||
// Check for hidden networks (often used in attacks)
|
||||
val hiddenCount = networks.count { it.ssid == "<Hidden>" }
|
||||
if (hiddenCount > 2) score += hiddenCount * 5
|
||||
|
||||
return score.coerceIn(0, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates deauthentication packet count based on network behavior.
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun estimateDeauthPackets(channel: Int, networks: List<WifiNetworkInfo>): Int {
|
||||
var estimatedPackets = 0
|
||||
|
||||
networks.forEach { network ->
|
||||
val history = networkHistory[network.bssid] ?: return@forEach
|
||||
if (history.size >= 3) {
|
||||
// Look for sudden disappearance patterns
|
||||
val rssiValues = history.takeLast(5).map { it.rssi }
|
||||
val suddenDrops = rssiValues.zipWithNext().count { (prev, curr) ->
|
||||
prev - curr > 15 // Sudden drop in signal
|
||||
}
|
||||
estimatedPackets += suddenDrops * 10
|
||||
|
||||
// Count rapid fluctuations
|
||||
val fluctuations = rssiValues.zipWithNext().count { (a, b) ->
|
||||
kotlin.math.abs(a - b) > 10
|
||||
}
|
||||
estimatedPackets += fluctuations * 5
|
||||
}
|
||||
}
|
||||
|
||||
return estimatedPackets
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects potential attacks based on scan results and channel stats.
|
||||
*/
|
||||
private fun detectAttacks(
|
||||
networks: List<WifiNetworkInfo>,
|
||||
stats: List<ChannelStats>
|
||||
): List<AttackEvent> {
|
||||
val attacks = mutableListOf<AttackEvent>()
|
||||
|
||||
// Check each channel for attack indicators
|
||||
stats.filter { it.suspiciousActivityScore >= 50 }.forEach { channelStat ->
|
||||
val channelNetworks = networks.filter { it.channel == channelStat.channel }
|
||||
|
||||
val attackType = when {
|
||||
channelStat.deauthPacketCount > 50 -> AttackType.DEAUTH
|
||||
channelNetworks.groupBy { it.ssid }.any { it.value.size > 2 } -> AttackType.EVIL_TWIN
|
||||
channelStat.networksCount > 20 -> AttackType.BEACON_FLOOD
|
||||
channelStat.suspiciousActivityScore >= 70 -> AttackType.UNKNOWN
|
||||
else -> null
|
||||
}
|
||||
|
||||
attackType?.let { type ->
|
||||
val targetNetwork = channelNetworks.maxByOrNull { it.rssi }
|
||||
attacks.add(
|
||||
AttackEvent(
|
||||
attackType = type,
|
||||
targetBssid = targetNetwork?.bssid,
|
||||
targetSsid = targetNetwork?.ssid,
|
||||
channel = channelStat.channel,
|
||||
signalStrength = targetNetwork?.rssi ?: -100,
|
||||
confidence = channelStat.suspiciousActivityScore
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return attacks
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all collected data and history.
|
||||
*/
|
||||
fun clearData() {
|
||||
_networks.value = emptyList()
|
||||
_channelStats.value = emptyList()
|
||||
_attacks.value = emptyList()
|
||||
networkHistory.clear()
|
||||
channelHistory.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the WiFi enabled state.
|
||||
*/
|
||||
fun isWifiEnabled(): Boolean = wifiManager.isWifiEnabled
|
||||
}
|
||||
|
||||
11
app/src/main/java/com/manalejandro/wifiattack/ui/theme/Color.kt
Archivo normal
@@ -0,0 +1,11 @@
|
||||
package com.manalejandro.wifiattack.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
58
app/src/main/java/com/manalejandro/wifiattack/ui/theme/Theme.kt
Archivo normal
@@ -0,0 +1,58 @@
|
||||
package com.manalejandro.wifiattack.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun WifiAttackTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
34
app/src/main/java/com/manalejandro/wifiattack/ui/theme/Type.kt
Archivo normal
@@ -0,0 +1,34 @@
|
||||
package com.manalejandro.wifiattack.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Archivo normal
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Archivo normal
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Archivo normal
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Archivo normal
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 7.6 KiB |
10
app/src/main/res/values/colors.xml
Archivo normal
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
3
app/src/main/res/values/strings.xml
Archivo normal
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">WifiAttack</string>
|
||||
</resources>
|
||||
5
app/src/main/res/values/themes.xml
Archivo normal
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.WifiAttack" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Archivo normal
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Archivo normal
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
17
app/src/test/java/com/manalejandro/wifiattack/ExampleUnitTest.kt
Archivo normal
@@ -0,0 +1,17 @@
|
||||
package com.manalejandro.wifiattack
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||