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