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

105
.gitignore vendido Archivo normal
Ver fichero

@@ -0,0 +1,105 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
.idea/caches
# Keystore files
*.jks
*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
lint/reports/
# Android Profiling
*.hprof
# Covers JetBrains IDEs
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS Credentials
*.credentials
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini

213
CONTRIBUTING.md Archivo normal
Ver fichero

@@ -0,0 +1,213 @@
# Contributing to WiFi Attack Detector
First off, thank you for considering contributing to WiFi Attack Detector! It's people like you that make this project better for everyone.
## Code of Conduct
By participating in this project, you are expected to uphold our Code of Conduct:
- Be respectful and inclusive
- Be patient and welcoming
- Be constructive in your feedback
- Focus on what is best for the community
## How Can I Contribute?
### Reporting Bugs
Before creating bug reports, please check existing issues to avoid duplicates. When creating a bug report, include:
- **Clear and descriptive title**
- **Steps to reproduce** the behavior
- **Expected behavior** vs **actual behavior**
- **Device information** (Android version, device model)
- **Screenshots** if applicable
- **Logs** if available
Use the bug report template:
```markdown
## Bug Description
[A clear description of the bug]
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. See error
## Expected Behavior
[What you expected to happen]
## Actual Behavior
[What actually happened]
## Environment
- Android Version: [e.g., Android 14]
- Device: [e.g., Pixel 7]
- App Version: [e.g., 1.0.0]
## Additional Context
[Any other relevant information]
```
### Suggesting Enhancements
Enhancement suggestions are welcome! Please provide:
- **Clear and descriptive title**
- **Detailed description** of the proposed feature
- **Use case** - why would this be useful?
- **Possible implementation** (optional)
### Pull Requests
1. **Fork** the repository
2. **Create a branch** from `main`:
```bash
git checkout -b feature/your-feature-name
```
3. **Make your changes** following our coding standards
4. **Test your changes** thoroughly
5. **Commit** with clear messages:
```bash
git commit -m "Add: Brief description of your changes"
```
6. **Push** to your fork:
```bash
git push origin feature/your-feature-name
```
7. **Create a Pull Request** against `main`
## Development Setup
### Prerequisites
- Android Studio Hedgehog or newer
- JDK 11 or higher
- Android SDK with API level 24+ installed
### Building the Project
```bash
# Clone your fork
git clone https://github.com/manalejandro/WifiAttack.git
cd WifiAttack
# Build debug APK
./gradlew assembleDebug
# Run tests
./gradlew test
# Run lint checks
./gradlew lint
```
### Project Structure
```
app/src/main/java/com/manalejandro/wifiattack/
├── MainActivity.kt # Entry point
├── data/model/ # Data classes
├── service/ # Background services
├── presentation/ # ViewModels and UI
└── ui/theme/ # Material 3 theming
```
## Coding Standards
### Kotlin Style Guide
Follow the [Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html):
- Use 4 spaces for indentation
- Maximum line length: 120 characters
- Use meaningful variable and function names
- Add KDoc comments for public APIs
### Compose Best Practices
- Keep composables small and focused
- Use `remember` and `derivedStateOf` appropriately
- Follow unidirectional data flow
- Use proper state hoisting
### Example Code Style
```kotlin
/**
* Displays a WiFi network card with signal strength indicator.
*
* @param network The network information to display
* @param onSelect Callback when the network is selected
* @param modifier Modifier for this composable
*/
@Composable
fun NetworkCard(
network: WifiNetworkInfo,
onSelect: (WifiNetworkInfo) -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
onClick = { onSelect(network) }
) {
// Card content
}
}
```
### Commit Message Format
Use conventional commits:
- `Add:` New feature
- `Fix:` Bug fix
- `Update:` Update existing feature
- `Refactor:` Code refactoring
- `Docs:` Documentation changes
- `Test:` Adding or updating tests
- `Chore:` Maintenance tasks
Example:
```
Add: Signal direction tracking with compass sensor
- Implement DirectionSensorManager for compass readings
- Add CompassView composable with visual indicator
- Track signal strength at different orientations
```
## Testing
### Running Tests
```bash
# Unit tests
./gradlew test
# Instrumented tests
./gradlew connectedAndroidTest
```
### Writing Tests
- Write unit tests for ViewModels and business logic
- Write UI tests for critical user flows
- Aim for meaningful test coverage
## Review Process
1. All PRs require at least one review
2. CI checks must pass
3. No merge conflicts
4. Code follows our standards
## Questions?
Feel free to open an issue with the "question" label or reach out to the maintainers.
---
Thank you for contributing! 🎉

191
LICENSE Archivo normal
Ver fichero

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024 manalejandro
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

89
PRIVACY.md Archivo normal
Ver fichero

@@ -0,0 +1,89 @@
# Privacy Policy for WiFi Attack Detector
**Last updated: December 2024**
## Overview
WiFi Attack Detector ("the App") is committed to protecting your privacy. This Privacy Policy explains our practices regarding data collection and usage.
## Data Collection
### What We DON'T Collect
WiFi Attack Detector does **NOT** collect, store, or transmit any of the following:
- Personal information (name, email, phone number)
- Location data (GPS coordinates)
- WiFi network passwords or credentials
- Device identifiers
- Usage analytics or statistics
- Any data to external servers
### What We DO Process Locally
The App processes the following data **only on your device**:
- **WiFi Scan Results**: Network names (SSIDs), signal strength, channel information, and MAC addresses (BSSIDs) of nearby access points
- **Sensor Data**: Compass heading and accelerometer readings for direction tracking
- **Attack Logs**: Locally stored logs of detected suspicious activity
**All this data remains on your device and is never transmitted externally.**
## Permissions
The App requires the following permissions:
### Location Permission
- **Why**: Android requires location permission to scan WiFi networks (this is an Android OS requirement, not ours)
- **How we use it**: Only to enable WiFi scanning; we never access or store your actual location
### WiFi Permissions
- **Why**: To scan and monitor nearby WiFi networks
- **How we use it**: Only for displaying network information and detecting anomalies
### Foreground Service Permission
- **Why**: To continue monitoring in the background when the App is minimized
- **How we use it**: Only for continuous WiFi monitoring as requested by the user
## Data Storage
- All data is stored locally on your device
- Data is cleared when you use the "Clear History" function
- Data is removed when you uninstall the App
- No cloud backup of App data is performed
## Data Sharing
We do **NOT** share any data with:
- Third parties
- Advertising networks
- Analytics services
- Any external servers
## Security
- The App operates entirely offline
- No internet connection is required or used
- All processing happens locally on your device
## Children's Privacy
The App does not knowingly collect any personal information from children under 13 years of age.
## Changes to This Policy
We may update this Privacy Policy from time to time. Changes will be reflected in the "Last updated" date above.
## Open Source
WiFi Attack Detector is open source. You can review the source code to verify our privacy practices:
- GitHub: [Repository URL]
## Contact
If you have questions about this Privacy Policy, please open an issue on our GitHub repository.
---
**Summary**: WiFi Attack Detector is a privacy-focused app that processes all data locally on your device. We don't collect, store, or transmit any personal information or usage data.

244
README.md Archivo normal
Ver fichero

@@ -0,0 +1,244 @@
# WiFi Attack Detector
<p align="center">
<img src="docs/images/app_icon.png" width="120" alt="WiFi Attack Detector Logo">
</p>
<p align="center">
<strong>Detect and track WiFi attacks in real-time using your Android device</strong>
</p>
<p align="center">
<a href="#features">Features</a> •
<a href="#installation">Installation</a> •
<a href="#usage">Usage</a> •
<a href="#how-it-works">How It Works</a> •
<a href="#contributing">Contributing</a> •
<a href="#license">License</a>
</p>
---
## Overview
WiFi Attack Detector is an Android application that monitors WiFi network activity to detect potential security threats. It analyzes WiFi scan results to identify suspicious patterns that may indicate various types of attacks, including deauthentication attacks, evil twin attacks, and beacon floods.
## Features
### 🔍 Real-time WiFi Monitoring
- Continuous scanning of nearby WiFi networks
- Detection of network anomalies and suspicious patterns
- Live updates with configurable scan intervals
### 📊 Channel Statistics
- Visual representation of activity per WiFi channel
- Error packet estimation based on network behavior
- Threat level indicators (None, Low, Medium, High, Critical)
- Support for 2.4GHz, 5GHz, and 6GHz bands
### ⚠️ Attack Detection
- **Deauthentication Attacks**: Detects sudden disconnection patterns
- **Evil Twin Attacks**: Identifies duplicate SSIDs on the same channel
- **Beacon Flood**: Detects excessive access point beacons
- **Probe Flood**: Identifies suspicious probe request activity
### 🧭 Signal Direction Tracking
- Uses device compass and accelerometer
- Records signal strength at different orientations
- Estimates the direction of attack sources
- Visual compass display with signal indicators
### 📱 Modern Android UI
- Material Design 3 (Material You)
- Jetpack Compose UI
- Dark/Light theme support
- Clean, intuitive interface
## Screenshots
<p align="center">
<img src="docs/images/dashboard.png" width="200" alt="Dashboard">
<img src="docs/images/channels.png" width="200" alt="Channel Stats">
<img src="docs/images/attacks.png" width="200" alt="Attack History">
<img src="docs/images/direction.png" width="200" alt="Direction Tracker">
</p>
## Requirements
- Android 7.0 (API level 24) or higher
- WiFi-enabled device
- Location permissions (required for WiFi scanning)
- Compass sensor (optional, for direction tracking)
## Installation
### From Source
1. Clone the repository:
```bash
git clone https://github.com/manalejandro/WifiAttack.git
cd WifiAttack
```
2. Open the project in Android Studio
3. Build and run on your device:
```bash
./gradlew assembleDebug
```
### From Release
Download the latest APK from the [Releases](https://github.com/yourusername/WifiAttack/releases) page.
## Usage
### Getting Started
1. **Grant Permissions**: On first launch, grant location and WiFi permissions when prompted
2. **Start Monitoring**: Tap the play button to begin WiFi scanning
3. **View Dashboard**: Monitor overall network status and threat level
4. **Explore Channels**: Navigate to "Channels" tab for detailed per-channel statistics
5. **Track Attacks**: View detected attacks in the "Attacks" tab
6. **Find Direction**: Use the "Direction" tab to locate signal sources
### Understanding Threat Levels
| Level | Color | Description |
|-------|-------|-------------|
| None | Green | No suspicious activity detected |
| Low | Light Green | Minor anomalies detected |
| Medium | Orange | Moderate suspicious activity |
| High | Deep Orange | Significant threat indicators |
| Critical | Red | Active attack likely in progress |
### Direction Tracking
1. Navigate to the "Direction" tab
2. Select a network to track from the list
3. Slowly rotate your device 360 degrees
4. The app will record signal strength at each direction
5. The strongest signal direction indicates the likely source location
## How It Works
### Detection Methods
WiFi Attack Detector uses heuristic analysis of WiFi scan results to detect potential attacks:
1. **Network Count Anomalies**: Sudden changes in the number of visible networks may indicate beacon flood attacks or deauthentication attempts
2. **RSSI Fluctuations**: Rapid changes in signal strength can indicate jamming or interference attacks
3. **Duplicate SSIDs**: Multiple access points with the same SSID on the same channel may indicate evil twin attacks
4. **Hidden Networks**: Excessive hidden networks can be indicators of attack infrastructure
### Limitations
⚠️ **Important**: This app uses publicly available Android APIs and cannot:
- Capture raw WiFi packets (requires root)
- See actual deauthentication frames
- Monitor encrypted traffic
The app provides **estimates** based on observable network behavior. For comprehensive WiFi security monitoring, consider dedicated hardware solutions.
## Architecture
```
com.manalejandro.wifiattack/
├── MainActivity.kt # Main activity and navigation
├── data/
│ └── model/
│ ├── WifiNetworkInfo.kt # Network data model
│ ├── ChannelStats.kt # Channel statistics model
│ └── AttackEvent.kt # Attack event model
├── service/
│ ├── WifiScannerService.kt # WiFi scanning and analysis
│ └── DirectionSensorManager.kt # Compass and direction tracking
├── presentation/
│ ├── WifiAttackViewModel.kt # Main ViewModel
│ └── screens/
│ ├── DashboardScreen.kt # Main dashboard
│ ├── ChannelStatsScreen.kt # Channel statistics
│ ├── AttacksScreen.kt # Attack history
│ └── DirectionScreen.kt # Signal direction tracker
└── ui/
└── theme/ # Material 3 theming
```
## Tech Stack
- **Language**: Kotlin
- **UI Framework**: Jetpack Compose
- **Architecture**: MVVM
- **Minimum SDK**: 24 (Android 7.0)
- **Target SDK**: 36
### Dependencies
- AndroidX Core KTX
- Jetpack Compose (BOM 2024.09.00)
- Material 3 Components
- Lifecycle ViewModel Compose
- Navigation Compose
- Kotlinx Coroutines
## Contributing
Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting pull requests.
### Development Setup
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/your-feature`
3. Make your changes
4. Run tests: `./gradlew test`
5. Submit a pull request
## Privacy
WiFi Attack Detector:
- Does NOT collect or transmit any user data
- Does NOT require internet connection
- Only uses WiFi scanning for local analysis
- All data remains on your device
See our [Privacy Policy](PRIVACY.md) for more details.
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
```
Copyright 2024 manalejandro
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
## Disclaimer
This application is provided for educational and security research purposes. Users are responsible for ensuring compliance with local laws and regulations regarding WiFi monitoring. The developers are not responsible for any misuse of this application.
## Acknowledgments
- [Material Design 3](https://m3.material.io/) for the design system
- [Jetpack Compose](https://developer.android.com/jetpack/compose) for the modern UI framework
- The Android security research community
---
<p align="center">
Made with ❤️ for network security awareness
</p>

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

6
build.gradle.kts Archivo normal
Ver fichero

@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

246
docs/ARCHITECTURE.md Archivo normal
Ver fichero

@@ -0,0 +1,246 @@
# Architecture Overview
## Application Architecture
WiFi Attack Detector follows the MVVM (Model-View-ViewModel) architecture pattern with clean separation of concerns.
```
┌─────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
│ │ MainActivity │────▶│ Composable Screens │ │
│ └────────┬────────┘ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ │Dashboard │ │ChannelStats │ │ │
│ ▼ │ └──────────┘ └──────────────┘ │ │
│ ┌─────────────────┐ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ WifiAttack │────▶│ │ Attacks │ │ Direction │ │ │
│ │ ViewModel │ │ └──────────┘ └──────────────┘ │ │
│ └────────┬────────┘ └──────────────────────────────────┘ │
│ │ │
└───────────┼──────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ Service Layer │
│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │
│ │ WifiScannerService │ │ DirectionSensorManager │ │
│ │ ┌───────────────────┐ │ │ ┌───────────────────────┐ │ │
│ │ │ WiFi Scanning │ │ │ │ Accelerometer │ │ │
│ │ │ Attack Detection │ │ │ │ Magnetometer │ │ │
│ │ │ Channel Analysis │ │ │ │ Direction Calculation │ │ │
│ │ └───────────────────┘ │ │ └───────────────────────┘ │ │
│ └─────────────────────────┘ └─────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ Data Layer │
│ ┌──────────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ WifiNetworkInfo │ │ ChannelStats │ │ AttackEvent │ │
│ └──────────────────┘ └───────────────┘ └──────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
```
## Component Details
### Presentation Layer
#### MainActivity
- Entry point of the application
- Handles permission requests
- Sets up Jetpack Compose content
- Provides ViewModel to composables
#### WifiAttackViewModel
- Central ViewModel managing all application state
- Exposes StateFlows for UI consumption
- Coordinates between WiFi scanning and direction tracking
- Handles user actions and navigation
#### Screens
- **DashboardScreen**: Overview with status, threat level, and quick stats
- **ChannelStatsScreen**: Detailed per-channel analysis with filtering
- **AttacksScreen**: Attack history with categorization
- **DirectionScreen**: Compass-based signal direction tracker
### Service Layer
#### WifiScannerService
- Manages WiFi scanning using `WifiManager`
- Registers `BroadcastReceiver` for scan results
- Analyzes scan results for anomalies
- Calculates channel statistics
- Detects potential attacks using heuristics
```kotlin
// Key Detection Heuristics
- Network count changes (beacon flood)
- RSSI fluctuations (jamming)
- Duplicate SSIDs (evil twin)
- Hidden network patterns
```
#### DirectionSensorManager
- Uses `SensorManager` for compass data
- Combines accelerometer and magnetometer readings
- Calculates device orientation (azimuth, pitch, roll)
- Records signal strength at different orientations
- Estimates signal source direction
### Data Layer
#### WifiNetworkInfo
```kotlin
data class WifiNetworkInfo(
val ssid: String,
val bssid: String,
val rssi: Int,
val frequency: Int,
val channel: Int,
val capabilities: String,
val timestamp: Long
)
```
#### ChannelStats
```kotlin
data class ChannelStats(
val channel: Int,
val band: WifiBand,
val networksCount: Int,
val averageRssi: Int,
val suspiciousActivityScore: Int,
val deauthPacketCount: Int,
val lastUpdateTime: Long
)
```
#### AttackEvent
```kotlin
data class AttackEvent(
val id: String,
val attackType: AttackType,
val targetBssid: String?,
val targetSsid: String?,
val channel: Int,
val estimatedDirection: Float?,
val signalStrength: Int,
val confidence: Int,
val timestamp: Long,
val isActive: Boolean
)
```
## Data Flow
```
┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ WifiManager │────▶│ WifiScannerService│────▶│ StateFlow<List> │
│ (Android OS) │ │ (Analysis Engine) │ │ (Reactive State) │
└──────────────┘ └───────────────────┘ └────────┬─────────┘
┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ SensorManager│────▶│DirectionSensor │────▶│ StateFlow<Float> │
│ (Android OS) │ │ Manager │ │ (Azimuth) │
└──────────────┘ └───────────────────┘ └────────┬─────────┘
┌───────────────────┐ ┌──────────────────┐
│ WifiAttackViewModel│◀───│ Collect as State │
└─────────┬─────────┘ └──────────────────┘
┌───────────────────┐
│ Composable UI │
│ (Recomposition) │
└───────────────────┘
```
## Attack Detection Algorithm
### Suspicious Activity Score Calculation
```
Score = 0
IF network_count_variance >= 5:
Score += 30
ELSE IF network_count_variance >= 3:
Score += 15
FOR each network:
IF rssi_variance >= 20:
Score += 20
ELSE IF rssi_variance >= 10:
Score += 10
FOR each ssid_group:
IF same_ssid_count > 1:
Score += same_ssid_count * 10
hidden_count = COUNT(hidden_networks)
IF hidden_count > 2:
Score += hidden_count * 5
RETURN CLAMP(Score, 0, 100)
```
### Attack Type Classification
| Condition | Attack Type |
|-----------|-------------|
| deauth_packets > 50 | Deauthentication |
| same_ssid_networks > 2 | Evil Twin |
| network_count > 20 | Beacon Flood |
| suspicious_score >= 70 | Unknown |
## Direction Tracking Algorithm
1. **Collect Readings**: Record RSSI at each device orientation
2. **Bucket Grouping**: Group readings into 10° buckets
3. **Average Calculation**: Calculate average RSSI per bucket
4. **Direction Estimation**: Find bucket with highest average RSSI
5. **Smoothing**: Apply moving average for stability
```
direction_buckets = GROUP_BY(readings, direction / 10 * 10)
bucket_averages = MAP(direction_buckets, AVERAGE(rssi))
estimated_direction = MAX_KEY(bucket_averages)
```
## Threading Model
```
Main Thread (UI)
├── Compose Recomposition
├── User Input Handling
ViewModel Scope (viewModelScope)
├── State Flow Collection
├── Coordination Logic
IO Dispatcher (Dispatchers.IO)
├── WiFi Scanning Loop
├── Broadcast Receiver Callbacks
Default Dispatcher
└── Sensor Callbacks (SensorManager)
```
## Future Improvements
1. **Root Detection Mode**: Access `/proc/net/wireless` for actual error counts
2. **Machine Learning**: Train model on attack patterns
3. **Persistent Storage**: Room database for historical analysis
4. **Background Service**: Foreground service for continuous monitoring
5. **Notifications**: Alert system for detected attacks
6. **Export Functionality**: Export logs for external analysis

85
docs/DEVELOPMENT.md Archivo normal
Ver fichero

@@ -0,0 +1,85 @@
# Development Setup Guide
## Prerequisites
### Java Version
This project requires **Java 17-21** for building. Java 25+ is not yet supported by Gradle 8.7.
If you have multiple Java versions installed, set `JAVA_HOME` before building:
```bash
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
./gradlew assembleDebug
```
### Android SDK
- Minimum SDK: 24 (Android 7.0)
- Target SDK: 35 (Android 15)
- Compile SDK: 35
### Required SDK Components
- Android SDK Platform 35
- Android SDK Build-Tools
- Android Emulator (optional, for testing)
## Building the Project
### Debug Build
```bash
./gradlew assembleDebug
```
The APK will be located at:
`app/build/outputs/apk/debug/app-debug.apk`
### Release Build
```bash
./gradlew assembleRelease
```
Note: Release builds require signing configuration.
## Running Tests
### Unit Tests
```bash
./gradlew test
```
### Instrumented Tests
```bash
./gradlew connectedAndroidTest
```
## IDE Setup
### Android Studio
1. Open Android Studio
2. Select "Open an existing project"
3. Navigate to the project directory
4. Wait for Gradle sync to complete
### IntelliJ IDEA
1. Open IntelliJ IDEA
2. Select "Import Project"
3. Choose the `build.gradle.kts` file
4. Select "Open as Project"
## Troubleshooting
### Build Fails with Java Version Error
If you see an error related to Java version (e.g., "25.0.1"):
1. Check your Java version: `java --version`
2. If using Java 25+, switch to Java 21
3. Set `JAVA_HOME` to point to Java 21
### Gradle Sync Issues
1. Clear Gradle caches: `./gradlew clean`
2. Invalidate caches in Android Studio: File > Invalidate Caches
3. Re-sync the project
### Missing SDK Components
Install required components through:
- Android Studio SDK Manager
- Or command line: `sdkmanager "platforms;android-35"`

23
gradle.properties Archivo normal
Ver fichero

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

40
gradle/libs.versions.toml Archivo normal
Ver fichero

@@ -0,0 +1,40 @@
[versions]
agp = "8.5.0"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
lifecycleViewModelCompose = "2.6.1"
navigationCompose = "2.7.5"
coroutines = "1.7.3"
materialIconsExtended = "1.5.4"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewModelCompose" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "materialIconsExtended" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

BIN
gradle/wrapper/gradle-wrapper.jar vendido Archivo normal

Archivo binario no mostrado.

8
gradle/wrapper/gradle-wrapper.properties vendido Archivo normal
Ver fichero

@@ -0,0 +1,8 @@
#Mon Dec 15 00:47:22 CET 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendido Archivo ejecutable
Ver fichero

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendido Archivo normal
Ver fichero

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

23
settings.gradle.kts Archivo normal
Ver fichero

@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "WifiAttack"
include(":app")