initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-12-15 02:53:23 +01:00
commit 2c3ac10798
Se han modificado 54 ficheros con 5170 adiciones y 0 borrados

1
app/.gitignore vendido Archivo normal
Ver fichero

@@ -0,0 +1 @@
/build

62
app/build.gradle.kts Archivo normal
Ver fichero

@@ -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
Ver fichero

@@ -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

Ver fichero

@@ -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)
}
}

Ver fichero

@@ -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>

Ver fichero

@@ -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()
)
}
}
}
}

Ver fichero

@@ -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")
}

Ver fichero

@@ -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
}

Ver fichero

@@ -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")
}

Ver fichero

@@ -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()
}
}

Ver fichero

@@ -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))
}

Ver fichero

@@ -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
)
}
}
}
}

Ver fichero

@@ -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))
}

Ver fichero

@@ -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
)
}

Ver fichero

@@ -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
)

Ver fichero

@@ -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
}

Ver fichero

@@ -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)

Ver fichero

@@ -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
)
}

Ver fichero

@@ -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
)
*/
)

Ver fichero

@@ -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>

Ver fichero

@@ -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>

Ver fichero

@@ -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>

Ver fichero

@@ -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>

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.4 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 2.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 982 B

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.7 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.9 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 3.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 2.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 5.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 3.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 7.6 KiB

Ver fichero

@@ -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>

Ver fichero

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">WifiAttack</string>
</resources>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.WifiAttack" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

Ver fichero

@@ -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>

Ver fichero

@@ -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>

Ver fichero

@@ -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)
}
}