diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..fea335d Binary files /dev/null and b/.DS_Store differ diff --git a/Team-Shivam/.DS_Store b/Team-Shivam/.DS_Store new file mode 100644 index 0000000..c3fc3e0 Binary files /dev/null and b/Team-Shivam/.DS_Store differ diff --git a/Team-Shivam/SheShield/.gitignore b/Team-Shivam/SheShield/.gitignore new file mode 100644 index 0000000..3a848e4 --- /dev/null +++ b/Team-Shivam/SheShield/.gitignore @@ -0,0 +1,37 @@ +# Flutter/Dart +.dart_tool/ +.packages +build/ +.flutter-plugins +.flutter-plugins-dependencies + +# Android +android/.gradle/ +android/app/build/ +android/build/ +**/android/**/GeneratedPluginRegistrant.java +*.apk + +# iOS +ios/Pods/ +ios/.symlinks/ +ios/Flutter/Flutter.framework +ios/Flutter/Flutter.podspec + +# IDE +.idea/ +.vscode/ +*.iml + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +*.lock +pubspec.lock diff --git a/Team-Shivam/SheShield/Get b/Team-Shivam/SheShield/Get new file mode 100644 index 0000000..e69de29 diff --git a/Team-Shivam/SheShield/README.md b/Team-Shivam/SheShield/README.md new file mode 100644 index 0000000..5c255ad --- /dev/null +++ b/Team-Shivam/SheShield/README.md @@ -0,0 +1,183 @@ +# She Shield – Smart Safety Wearable System πŸ›‘οΈ + +SheShield is a women's safety wearable system developed for HackHerThon 2026. It consists of a **smart safety bracelet** (ESP32 hardware) and a **Flutter mobile app** (emergency response). The bracelet detects danger via SOS button and shake/motion sensor, then triggers the phone app over **Bluetooth Classic (SPP)** to send alerts, share location, record video, and contact emergency services. + +## **πŸ“Ή Prototype & App Demonstration Video** + +### **[β–Ά Click here to watch the full demo on Google Drive](https://drive.google.com/file/d/1BB_w9BKbrbSptshWBkR0CEz_fbgr8N_p/view?usp=sharing)** + +--- + +## System Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Bluetooth Classic (SPP) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ESP32 Bracelet β”‚ ──────────────────────────────────▢ β”‚ Flutter App β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β€’ SOS Button β”‚ "SOS\n" / "SHAKE\n" β”‚ β€’ SOS Alert UI β”‚ +β”‚ β€’ Accelerometer β”‚ ◀────────────────────────────────── β”‚ β€’ SMS to Contacts β”‚ +β”‚ β€’ Buzzer β”‚ "BUZZER_ON\n" / "LED_ON\n" β”‚ β€’ Live Location β”‚ +β”‚ β€’ LED β”‚ β”‚ β€’ Video Recording β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β€’ Police Stations β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +| Component | Role | +|---|---| +| **ESP32 Bracelet (Hardware)** | Trigger β€” SOS button, accelerometer shake, buzzer, LED | +| **Mobile App (Software)** | Response β€” GPS, alerts, SMS, video, maps, notifications | + +--- + +## Features + +### πŸ”§ ESP32 Bracelet (Trigger Device) +- πŸ”˜ **SOS Button** β€” Press to send `"SOS\n"` to the app +- πŸ“³ **Shake Detection** β€” MPU6050 accelerometer auto-sends `"SHAKE\n"` +- πŸ”Š **Remote Buzzer** β€” App can trigger buzzer via `"BUZZER_ON\n"` +- πŸ’‘ **Remote LED** β€” App controls LED via `"LED_ON\n"` / `"LED_OFF\n"` +- πŸ“‘ **Bluetooth Classic** β€” Broadcasts as `"SheShield"` via SPP/RFCOMM + +### πŸ“± Mobile App (Response System) +- πŸ” **Login / Sign-Up** β€” Firebase email/password authentication +- πŸ”΄ **SOS Button** β€” 3-second hold to prevent accidental triggers +- πŸ“ **Live Location** β€” GPS tracking with Google Maps (updates every 5s) +- πŸ“€ **Share Location** β€” WhatsApp, SMS, or clipboard +- πŸš” **Nearby Police Stations** β€” OpenStreetMap Overpass API, sorted by distance +- πŸ‘₯ **Emergency Contacts** β€” Add, delete, send SOS SMS to all with location link +- πŸ“‘ **Bluetooth Classic Pairing** β€” Connect to ESP32, persistent connection across screens +- πŸŽ™οΈ **Voice Commands** β€” Phone mic detects "help", "bachao", "danger", "police" +- πŸ“Ή **SOS Video Recording** β€” Auto-records 30s video, uploads to Firebase Storage +- πŸ”” **Push Notifications** β€” Firebase Cloud Messaging alerts +- πŸ—‚οΈ **Past Emergencies** β€” View history of triggered SOS events + +### πŸ”— Persistent Bluetooth Connection +- **Singleton service** β€” Connection stays alive across all screens +- **Global SOS listener** β€” Triggers emergency from any screen +- **Only disconnects** when user explicitly taps "Disconnect" + +--- + +## Setup + +### Prerequisites +- [Flutter SDK](https://docs.flutter.dev/get-started/install) (3.0+) +- Android Studio or VS Code with Flutter plugin +- Firebase project (Auth, Firestore, Storage, FCM) +- Google Maps API key +- [Arduino IDE](https://www.arduino.cc/en/software) (for ESP32) + +### 1. Install Flutter Dependencies + +```bash +cd sheshield +flutter pub get +``` + +### 2. Firebase Setup +- Add your `google-services.json` to `android/app/` +- Enable: Authentication (Email/Password), Firestore, Storage, Cloud Messaging + +### 3. Google Maps API Key +Already configured in `AndroidManifest.xml`. + +### 4. ESP32 Setup +1. Open `esp32/sheshield_band.ino` in Arduino IDE +2. Install ESP32 board via Board Manager +3. **Update pin numbers** at the top if your wiring differs: + +| Component | Default GPIO | +|-----------|-------------| +| Button | 4 | +| Buzzer | 5 | +| LED | 2 | +| MPU6050 SDA | 21 | +| MPU6050 SCL | 22 | + +4. Upload to your ESP32 + +### 5. Run the App + +```bash +flutter run +``` + +--- + +## Project Structure + +``` +sheshield/ +β”œβ”€β”€ lib/ +β”‚ β”œβ”€β”€ main.dart # App entry, auth routing, dark theme +β”‚ β”œβ”€β”€ screens/ +β”‚ β”‚ β”œβ”€β”€ login_screen.dart # Email/password login & sign-up +β”‚ β”‚ β”œβ”€β”€ home_screen.dart # SOS button, status, GPS, navigation +β”‚ β”‚ β”œβ”€β”€ location_screen.dart # Google Maps, live tracking, share +β”‚ β”‚ β”œβ”€β”€ contacts_screen.dart # Emergency contacts management +β”‚ β”‚ β”œβ”€β”€ nearby_police_screen.dart # Nearby police stations map +β”‚ β”‚ β”œβ”€β”€ bluetooth_screen.dart # Bluetooth Classic pairing UI +β”‚ β”‚ β”œβ”€β”€ past_emergencies_screen.dart # SOS event history +β”‚ β”‚ └── profile_screen.dart # User profile +β”‚ └── services/ +β”‚ β”œβ”€β”€ bracelet_service.dart # Bluetooth Classic singleton (persistent) +β”‚ β”œβ”€β”€ location_service.dart # Geolocator GPS wrapper +β”‚ β”œβ”€β”€ sms_service.dart # Send SOS SMS to contacts +β”‚ β”œβ”€β”€ video_recording_service.dart # 30s video recording + upload +β”‚ β”œβ”€β”€ notification_service.dart # FCM push notifications +β”‚ β”œβ”€β”€ alert_service.dart # Firestore SOS alerts +β”‚ β”œβ”€β”€ places_service.dart # OpenStreetMap police stations +β”‚ β”œβ”€β”€ voice_trigger_service.dart # Speech-to-text voice commands +β”‚ └── storage_service.dart # SharedPreferences contacts +β”œβ”€β”€ esp32/ +β”‚ └── sheshield_band.ino # ESP32 Bluetooth Classic sketch +β”œβ”€β”€ android/ +β”‚ └── app/src/main/AndroidManifest.xml +β”œβ”€β”€ pubspec.yaml +└── README.md +``` + +--- + +## Dependencies + +| Package | Purpose | +|---|---| +| `firebase_core` | Firebase initialization | +| `firebase_auth` | Email/password authentication | +| `cloud_firestore` | SOS alerts & location storage | +| `firebase_storage` | Video upload | +| `firebase_messaging` | Push notifications | +| `geolocator` | GPS location access | +| `google_maps_flutter` | Map display | +| `flutter_bluetooth_serial_ble` | Bluetooth Classic (SPP/RFCOMM) | +| `speech_to_text` | Voice command detection | +| `camera` | Video recording | +| `shared_preferences` | Local contact storage | +| `permission_handler` | Runtime permissions | +| `url_launcher` | Open URLs, SMS | +| `http` | HTTP requests | + +--- + +## ESP32 Communication Protocol + +### ESP32 β†’ App (Serial) +| Command | Trigger | +|---------|---------| +| `SOS\n` | Button pressed | +| `SHAKE\n` | Motion detected | + +### App β†’ ESP32 (Serial) +| Command | Action | +|---------|--------| +| `BUZZER_ON\n` | Activate buzzer | +| `BUZZER_OFF\n` | Stop buzzer | +| `LED_ON\n` | Turn LED on | +| `LED_OFF\n` | Turn LED off | + +--- + +## License + +Built for HackHerThon 2026 πŸ† diff --git a/Team-Shivam/SheShield/Run b/Team-Shivam/SheShield/Run new file mode 100644 index 0000000..e69de29 diff --git a/Team-Shivam/SheShield/SYSTEM_ARCHITECTURE.md b/Team-Shivam/SheShield/SYSTEM_ARCHITECTURE.md new file mode 100644 index 0000000..59049b9 --- /dev/null +++ b/Team-Shivam/SheShield/SYSTEM_ARCHITECTURE.md @@ -0,0 +1,244 @@ +# SheShield β€” System Architecture + +## Overview + +SheShield is a smart wearable safety system comprising a **hardware bracelet** (ESP32-based) and a **Flutter mobile app**. When an emergency is detected β€” via button press, shake gesture, or voice keywords β€” the system triggers multi-channel SOS alerts including SMS, live location sharing, push notifications, and audio/video recording. + +--- + +## High-Level Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ SHESHIELD SYSTEM β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ HARDWARE (Bracelet) β”‚ SOFTWARE (Mobile App) β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ESP32 MCU │◄─┼─── Bluetooth Classic (SPP) β”‚ β”‚ +β”‚ β”‚ (Main Brain) │──┼─►│ BluetoothService Singleton β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Sensors & I/O β”‚ β”‚ β”‚ Alert Engine β”‚ β”‚ +β”‚ β”‚ β€’ Push Button β”‚ β”‚ β”‚ β€’ Location Service (GPS) β”‚ β”‚ +β”‚ β”‚ β€’ Accelerometer β”‚ β”‚ β”‚ β€’ SMS Service β”‚ β”‚ +β”‚ β”‚ β€’ Buzzer/Alarm β”‚ β”‚ β”‚ β€’ Push Notifications (FCM) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β€’ Video Recording β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Alert Broadcast (Firestore) β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ Power System β”‚ β”‚ β”‚ +β”‚ β”‚ β€’ Li-Po Battery β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β€’ TP4056 Chargerβ”‚ β”‚ β”‚ Voice Trigger Engine β”‚ β”‚ +β”‚ β”‚ β€’ USB-C Port β”‚ β”‚ β”‚ β€’ Speech-to-Text (On-device) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β€’ Keyword Detection β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ SIM Module β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β€’ SIM800L GSM β”‚ β”‚ β”‚ Firebase Backend β”‚ β”‚ +β”‚ β”‚ β€’ SMS Fallback β”‚ β”‚ β”‚ β€’ Auth / Firestore / Storage β”‚ β”‚ +β”‚ β”‚ β€’ Emergency Callβ”‚ β”‚ β”‚ β€’ Cloud Messaging (FCM) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Hardware Architecture + +### ESP32 Microcontroller (Main Brain) +| Spec | Detail | +|------|--------| +| **Chip** | ESP32-WROOM-32 | +| **Clock** | 240 MHz dual-core | +| **Flash** | 4 MB | +| **Connectivity** | Bluetooth Classic (SPP) + WiFi | +| **GPIO Used** | Button (GPIO 13), Buzzer (GPIO 25), IMU (I2C) | + +### Sensors & Input/Output +| Component | Purpose | Interface | +|-----------|---------|-----------| +| **Push Button** | Manual SOS trigger | GPIO 13 (pull-up) | +| **MPU6050 Accelerometer** | Shake/impact detection | I2C (SDA/SCL) | +| **Piezo Buzzer** | Audible alarm on SOS | GPIO 25 (PWM) | +| **LED Indicator** | Connection/SOS status | GPIO 2 (built-in) | + +### Power System +| Component | Specification | +|-----------|--------------| +| **Battery** | 3.7V 500mAh Li-Po (rechargeable) | +| **Charging IC** | TP4056 (1A linear charger) | +| **Charging Port** | Micro-USB / USB-C | +| **Protection** | Overcharge, over-discharge, short-circuit (DW01A + FS8205A) | +| **Voltage Regulator** | AMS1117-3.3V (stable 3.3V to ESP32) | +| **Battery Life** | ~8–12 hours (BT active, idle) | +| **Charge Time** | ~1.5 hours (0β†’100%) | + +### SIM / GSM Module +| Component | Specification | +|-----------|--------------| +| **Module** | SIM800L GSM/GPRS | +| **Band** | Quad-band 850/900/1800/1900 MHz | +| **SIM Type** | Nano-SIM / Micro-SIM | +| **Power** | 3.4V–4.4V (separate LDO from battery) | +| **Functions** | SMS fallback, emergency call (112/100) | +| **Antenna** | PCB helical antenna | +| **Interface** | UART (TXβ†’GPIO 16, RXβ†’GPIO 17) | + +### ESP32 Firmware Commands (sent over Bluetooth) +``` +BUTTON SOS β†’ Button pressed, trigger SOS +MOTION SOS β†’ Shake/impact detected, trigger SOS +VOICE SOS β†’ Reserved for voice relay +ACK β†’ Acknowledgment +``` + +### Hardware Block Diagram + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + USB-C ───►│ TP4056 β”‚ + β”‚ Charger IC β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ VBAT + β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” + β”‚ Li-Po β”‚ + β”‚ 3.7V 500mAhβ”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” + β”‚ AMS1117-3.3V│◄── Voltage Regulator + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ 3.3V + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”΄β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” + β”‚ ESP32 β”‚ β”‚MPU β”‚ β”‚ SIM800L β”‚ + β”‚ MCU β”‚ β”‚6050 β”‚ β”‚ GSM β”‚ + β”‚ β”‚ β”‚(I2C) β”‚ β”‚ Module β”‚ + β”‚ BT Classic β”‚ β””β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ + β”‚ (to Phone) β”‚ β”‚ Nano-SIM β”‚ + β””β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”˜ └────┐ + β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” +β”‚ Button β”‚ β”‚ Buzzer β”‚ +β”‚ (GPIO13)β”‚ β”‚ (GPIO 25) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Software Architecture + +### App Layer Stack + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ UI Layer (Screens) β”‚ +β”‚ HomeScreen β”‚ SOSScreen β”‚ BluetoothScreenβ”‚ +β”‚ LoginScreenβ”‚ ProfileScreen β”‚ MapScreen β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Service Layer β”‚ +β”‚ BluetoothService β”‚ LocationService β”‚ +β”‚ AlertService β”‚ SmsService β”‚ +β”‚ NotificationServiceβ”‚ VideoRecording β”‚ +β”‚ VoiceTriggerServiceβ”‚ PlacesService β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Backend (Firebase) β”‚ +β”‚ Auth β”‚ Firestore β”‚ Storage β”‚ FCM β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Platform (Android) β”‚ +β”‚ Bluetooth SPP β”‚ GPS β”‚ Camera β”‚ SMS β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Key Services + +| Service | File | Responsibility | +|---------|------|----------------| +| **BluetoothService** | `bluetooth_service.dart` | Singleton, auto-reconnect, command parsing, buzzer control | +| **LocationService** | `location_service.dart` | GPS tracking, live Firestore updates every 5s | +| **AlertService** | `alert_service.dart` | Writes SOS alerts to Firestore `alerts` collection | +| **SmsService** | `sms_service.dart` | Sends SMS with Google Maps link to emergency contacts | +| **NotificationService** | `notification_service.dart` | FCM push notifications to emergency contacts | +| **VideoRecordingService** | `video_recording_service.dart` | Camera recording, uploads to Firebase Storage | +| **VoiceTriggerService** | `voice_trigger_service.dart` | Continuous speech recognition, keyword matching | +| **PlacesService** | `places_service.dart` | Overpass API β†’ nearby police stations (100km radius) | + +--- + +## SOS Trigger Flow + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ BUTTON PRESS β”‚ β”‚ SHAKE DETECT β”‚ β”‚ VOICE KEYWORD β”‚ + β”‚ (Hardware) β”‚ β”‚ (Hardware) β”‚ β”‚ (App Mic) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β”‚ "BUTTON SOS" β”‚ "MOTION SOS" β”‚ keyword match + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ Bluetooth SPP β”‚ + β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ BluetoothService.onCommand β”‚ + β”‚ _triggerGlobalSOS() β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Location β”‚ β”‚ SMS to β”‚ β”‚ FCM Push β”‚ + β”‚ to Firestoreβ”‚ β”‚ Contacts β”‚ β”‚ Notif β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Video β”‚ β”‚ Alert to β”‚ β”‚ Buzzer β”‚ + β”‚ Recording β”‚ β”‚ Firestore β”‚ β”‚ Activate β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Full-Screen SOS UI β”‚ + β”‚ (Flashing Red Alert) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Communication Protocol + +| Layer | Protocol | Details | +|-------|----------|---------| +| **Bracelet ↔ Phone** | Bluetooth Classic SPP | 9600 baud, UTF-8 newline-delimited | +| **Phone ↔ Firebase** | HTTPS / WebSocket | Firestore real-time streams | +| **Phone ↔ Contacts** | SMS (native) | Google Maps link included | +| **Phone ↔ Contacts** | FCM Push | Via Firebase Cloud Functions | +| **Bracelet ↔ Network** | GSM (SIM800L) | SMS fallback if phone disconnected | + +--- + +## Security & Privacy + +- Firebase Authentication (email/password) +- Firestore security rules (user-scoped data) +- Location data encrypted in transit (HTTPS/TLS) +- Emergency contacts stored locally (SharedPreferences) +- No data shared with third parties + +--- + +## Tech Stack Summary + +| Category | Technology | +|----------|-----------| +| **Hardware** | ESP32, MPU6050, SIM800L, TP4056, Li-Po | +| **Mobile** | Flutter (Dart) | +| **Backend** | Firebase (Auth, Firestore, Storage, FCM) | +| **Maps** | Google Maps Flutter + Overpass API | +| **Bluetooth** | flutter_bluetooth_serial_ble (Classic SPP) | +| **Speech** | speech_to_text (on-device) | +| **Camera** | camera package (auto-record on SOS) | diff --git a/Team-Shivam/SheShield/Stellaris-Hackathon b/Team-Shivam/SheShield/Stellaris-Hackathon new file mode 160000 index 0000000..787ed03 --- /dev/null +++ b/Team-Shivam/SheShield/Stellaris-Hackathon @@ -0,0 +1 @@ +Subproject commit 787ed038b5ed37f926a21847201d0f88dfe65a02 diff --git a/Team-Shivam/SheShield/analysis_options.yaml b/Team-Shivam/SheShield/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/Team-Shivam/SheShield/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/Team-Shivam/SheShield/android/.gitignore b/Team-Shivam/SheShield/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/Team-Shivam/SheShield/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/Team-Shivam/SheShield/android/app/build.gradle.kts b/Team-Shivam/SheShield/android/app/build.gradle.kts new file mode 100644 index 0000000..23634a5 --- /dev/null +++ b/Team-Shivam/SheShield/android/app/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") + id("com.google.gms.google-services") +} + +android { + namespace = "com.sheshield.sheshield" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.sheshield.sheshield" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + implementation("androidx.concurrent:concurrent-futures:1.2.0") +} + +flutter { + source = "../.." +} diff --git a/Team-Shivam/SheShield/android/app/google-services.json b/Team-Shivam/SheShield/android/app/google-services.json new file mode 100644 index 0000000..dad0f53 --- /dev/null +++ b/Team-Shivam/SheShield/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "898358425014", + "project_id": "sheshield-ea683", + "storage_bucket": "sheshield-ea683.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:898358425014:android:02ebb5555ee0bb9f9f3d17", + "android_client_info": { + "package_name": "com.sheshield.sheshield" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyBDnxhDACM9N698Q3Z54YPPisAhH6FzYj8" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/Team-Shivam/SheShield/android/app/src/debug/AndroidManifest.xml b/Team-Shivam/SheShield/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Team-Shivam/SheShield/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Team-Shivam/SheShield/android/app/src/main/AndroidManifest.xml b/Team-Shivam/SheShield/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dcf2d06 --- /dev/null +++ b/Team-Shivam/SheShield/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Team-Shivam/SheShield/android/app/src/main/kotlin/com/sheshield/sheshield/MainActivity.kt b/Team-Shivam/SheShield/android/app/src/main/kotlin/com/sheshield/sheshield/MainActivity.kt new file mode 100644 index 0000000..1f04300 --- /dev/null +++ b/Team-Shivam/SheShield/android/app/src/main/kotlin/com/sheshield/sheshield/MainActivity.kt @@ -0,0 +1,37 @@ +package com.sheshield.sheshield + +import android.telephony.SmsManager +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + +class MainActivity : FlutterActivity() { + private val CHANNEL = "com.sheshield/sms" + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + .setMethodCallHandler { call, result -> + if (call.method == "sendSms") { + val phone = call.argument("phone") + val message = call.argument("message") + + if (phone != null && message != null) { + try { + val smsManager = SmsManager.getDefault() + val parts = smsManager.divideMessage(message) + smsManager.sendMultipartTextMessage(phone, null, parts, null, null) + result.success(true) + } catch (e: Exception) { + result.error("SMS_ERROR", e.message, null) + } + } else { + result.error("INVALID_ARGS", "Phone or message is null", null) + } + } else { + result.notImplemented() + } + } + } +} diff --git a/Team-Shivam/SheShield/android/app/src/main/res/drawable-v21/launch_background.xml b/Team-Shivam/SheShield/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..c1083fa --- /dev/null +++ b/Team-Shivam/SheShield/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/Team-Shivam/SheShield/android/app/src/main/res/drawable-xxhdpi/splash_logo.png b/Team-Shivam/SheShield/android/app/src/main/res/drawable-xxhdpi/splash_logo.png new file mode 100644 index 0000000..83e2cd1 Binary files /dev/null and b/Team-Shivam/SheShield/android/app/src/main/res/drawable-xxhdpi/splash_logo.png differ diff --git a/Team-Shivam/SheShield/android/app/src/main/res/drawable/launch_background.xml b/Team-Shivam/SheShield/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..62b5792 --- /dev/null +++ b/Team-Shivam/SheShield/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/Team-Shivam/SheShield/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..4de5ae2 Binary files /dev/null and b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Team-Shivam/SheShield/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..e1f0171 Binary files /dev/null and b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/Team-Shivam/SheShield/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..a7d7224 Binary files /dev/null and b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/Team-Shivam/SheShield/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..9648387 Binary files /dev/null and b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Team-Shivam/SheShield/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..75938aa Binary files /dev/null and b/Team-Shivam/SheShield/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Team-Shivam/SheShield/android/app/src/main/res/values-night/styles.xml b/Team-Shivam/SheShield/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/Team-Shivam/SheShield/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Team-Shivam/SheShield/android/app/src/main/res/values/styles.xml b/Team-Shivam/SheShield/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/Team-Shivam/SheShield/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Team-Shivam/SheShield/android/app/src/profile/AndroidManifest.xml b/Team-Shivam/SheShield/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Team-Shivam/SheShield/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Team-Shivam/SheShield/android/build.gradle.kts b/Team-Shivam/SheShield/android/build.gradle.kts new file mode 100644 index 0000000..14c3ed2 --- /dev/null +++ b/Team-Shivam/SheShield/android/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("com.google.gms.google-services") version "4.4.2" apply false +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/Team-Shivam/SheShield/android/gradle.properties b/Team-Shivam/SheShield/android/gradle.properties new file mode 100644 index 0000000..505dc50 --- /dev/null +++ b/Team-Shivam/SheShield/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true diff --git a/Team-Shivam/SheShield/android/gradle/wrapper/gradle-wrapper.properties b/Team-Shivam/SheShield/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/Team-Shivam/SheShield/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/Team-Shivam/SheShield/android/settings.gradle.kts b/Team-Shivam/SheShield/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/Team-Shivam/SheShield/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/Team-Shivam/SheShield/assets/images/logo.png b/Team-Shivam/SheShield/assets/images/logo.png new file mode 100644 index 0000000..3c8d6ae Binary files /dev/null and b/Team-Shivam/SheShield/assets/images/logo.png differ diff --git a/Team-Shivam/SheShield/com.android.builder.sdk.LicenceNotAcceptedException: b/Team-Shivam/SheShield/com.android.builder.sdk.LicenceNotAcceptedException: new file mode 100644 index 0000000..e69de29 diff --git a/Team-Shivam/SheShield/esp32/sheshield_band.ino b/Team-Shivam/SheShield/esp32/sheshield_band.ino new file mode 100644 index 0000000..5da97b8 --- /dev/null +++ b/Team-Shivam/SheShield/esp32/sheshield_band.ino @@ -0,0 +1,229 @@ +/* + * SheShield ESP32 β€” Bluetooth Classic (Serial) + * ============================================== + * + * Components: + * - Push Button (Switch) β†’ GPIO 4 (triggers SOS) + * - Buzzer β†’ GPIO 5 (alert from app) + * - LED β†’ GPIO 2 (status indicator) + * - MPU6050 Accelerometerβ†’ SDA=21, SCL=22 (shake detection) + * + * Broadcasts as "SheShield" via Bluetooth Classic (SPP). + * + * Protocol β€” ESP32 β†’ App (Serial strings): + * "SOS\n" β†’ SOS button pressed + * "SHAKE\n" β†’ Shake detected + * + * Protocol β€” App β†’ ESP32 (Serial strings): + * "BUZZER_ON\n" β†’ Activate buzzer + * "BUZZER_OFF\n" β†’ Stop buzzer + * "LED_ON\n" β†’ Turn LED on + * "LED_OFF\n" β†’ Turn LED off + * + * ⚠️ CHANGE THE PIN NUMBERS BELOW IF YOUR WIRING IS DIFFERENT! + */ + +#include "BluetoothSerial.h" +#include + +BluetoothSerial SerialBT; + +// ==================== PIN CONFIGURATION ==================== +// ⚠️ CHANGE THESE TO MATCH YOUR WIRING +#define BUTTON_PIN 4 // Push button / switch +#define BUZZER_PIN 5 // Buzzer +#define LED_PIN 2 // LED (GPIO 2 = built-in on most ESP32) +#define MPU_SDA 21 // Accelerometer SDA +#define MPU_SCL 22 // Accelerometer SCL + +// ==================== SHAKE DETECTION ==================== +#define MPU6050_ADDR 0x68 +#define SHAKE_THRESHOLD 18000 // Adjust: higher = less sensitive +#define SHAKE_COOLDOWN_MS 3000 // Min ms between shake alerts + +// ==================== STATE ==================== +unsigned long lastShakeTime = 0; +unsigned long lastButtonPress = 0; +bool buzzerActive = false; +int buzzerBeepCount = 0; +unsigned long buzzerTimer = 0; +String inputBuffer = ""; + +// ==================== MPU6050 SETUP ==================== +bool mpuAvailable = false; + +void setupMPU6050() { + Wire.begin(MPU_SDA, MPU_SCL); + Wire.beginTransmission(MPU6050_ADDR); + Wire.write(0x6B); // Power management + Wire.write(0); // Wake up + byte error = Wire.endTransmission(true); + + if (error == 0) { + mpuAvailable = true; + Serial.println("βœ… MPU6050 found!"); + } else { + mpuAvailable = false; + Serial.println("⚠️ MPU6050 not found β€” shake detection disabled"); + } +} + +// ==================== READ ACCELEROMETER ==================== +bool detectShake() { + if (!mpuAvailable) + return false; + + Wire.beginTransmission(MPU6050_ADDR); + Wire.write(0x3B); + if (Wire.endTransmission(false) != 0) + return false; + + Wire.requestFrom((int)MPU6050_ADDR, 6, (int)true); + if (Wire.available() < 6) + return false; + + int16_t ax = Wire.read() << 8 | Wire.read(); + int16_t ay = Wire.read() << 8 | Wire.read(); + int16_t az = Wire.read() << 8 | Wire.read(); + + float magnitude = + sqrt((float)(ax * ax) + (float)(ay * ay) + (float)(az * az)); + return magnitude > SHAKE_THRESHOLD; +} + +// ==================== HANDLE APP COMMANDS ==================== +void handleAppCommand(String cmd) { + cmd.trim(); + Serial.printf("πŸ“₯ App command: %s\n", cmd.c_str()); + + if (cmd == "BUZZER_ON") { + buzzerActive = true; + buzzerBeepCount = 6; + buzzerTimer = millis(); + Serial.println("πŸ”Š Buzzer ON"); + } else if (cmd == "BUZZER_OFF") { + buzzerActive = false; + noTone(BUZZER_PIN); + Serial.println("πŸ”‡ Buzzer OFF"); + } else if (cmd == "LED_ON") { + digitalWrite(LED_PIN, HIGH); + Serial.println("πŸ’‘ LED ON"); + } else if (cmd == "LED_OFF") { + digitalWrite(LED_PIN, LOW); + Serial.println("πŸ’‘ LED OFF"); + } +} + +// ==================== BUZZER HANDLER ==================== +void handleBuzzer() { + if (!buzzerActive || buzzerBeepCount <= 0) + return; + + unsigned long now = millis(); + if (now - buzzerTimer > 300) { + buzzerTimer = now; + buzzerBeepCount--; + + if (buzzerBeepCount % 2 == 1) { + tone(BUZZER_PIN, 2500); + } else { + noTone(BUZZER_PIN); + } + + if (buzzerBeepCount <= 0) { + buzzerActive = false; + noTone(BUZZER_PIN); + } + } +} + +// ==================== SETUP ==================== +void setup() { + Serial.begin(115200); + Serial.println("\nπŸ›‘οΈ SheShield Band Starting..."); + + // Configure pins + pinMode(BUTTON_PIN, INPUT_PULLUP); + pinMode(BUZZER_PIN, OUTPUT); + pinMode(LED_PIN, OUTPUT); + + // Startup indicator + digitalWrite(LED_PIN, HIGH); + tone(BUZZER_PIN, 1000, 300); + delay(500); + digitalWrite(LED_PIN, LOW); + + // Initialize accelerometer + setupMPU6050(); + + // Initialize Bluetooth Classic + SerialBT.begin("SheShield"); // Device name shown in scan + Serial.println("βœ… Bluetooth Classic started as: SheShield"); + Serial.println("πŸ›‘οΈ SheShield Band Ready!\n"); +} + +// ==================== LOOP ==================== +void loop() { + unsigned long now = millis(); + + // --- Button press detection (SOS) --- + if (digitalRead(BUTTON_PIN) == LOW) { // Active LOW with pull-up + if (now - lastButtonPress > 1000) { // 1s debounce + lastButtonPress = now; + Serial.println("🚨 SOS BUTTON PRESSED!"); + + // Send to app + SerialBT.println("SOS"); + + // Feedback + digitalWrite(LED_PIN, HIGH); + tone(BUZZER_PIN, 2000, 200); + delay(200); + digitalWrite(LED_PIN, LOW); + } + } + + // --- Shake detection --- + if (now - lastShakeTime > SHAKE_COOLDOWN_MS) { + if (detectShake()) { + lastShakeTime = now; + Serial.println("πŸ“³ SHAKE DETECTED!"); + + // Send to app + SerialBT.println("SHAKE"); + + // Feedback + digitalWrite(LED_PIN, HIGH); + tone(BUZZER_PIN, 1500, 150); + delay(150); + digitalWrite(LED_PIN, LOW); + } + } + + // --- Read commands from app --- + while (SerialBT.available()) { + char c = SerialBT.read(); + if (c == '\n') { + handleAppCommand(inputBuffer); + inputBuffer = ""; + } else if (c != '\r') { + inputBuffer += c; + } + } + + // --- Handle buzzer beeping --- + handleBuzzer(); + + // --- Status LED blink when connected --- + if (SerialBT.hasClient()) { + static unsigned long ledTimer = 0; + if (now - ledTimer > 3000) { + ledTimer = now; + digitalWrite(LED_PIN, HIGH); + delay(50); + digitalWrite(LED_PIN, LOW); + } + } + + delay(50); +} diff --git a/Team-Shivam/SheShield/lib/main.dart b/Team-Shivam/SheShield/lib/main.dart new file mode 100644 index 0000000..fa29ce7 --- /dev/null +++ b/Team-Shivam/SheShield/lib/main.dart @@ -0,0 +1,552 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'screens/home_screen.dart'; +import 'screens/login_screen.dart'; +import 'screens/sos_screen.dart'; +import 'screens/security_setup_screen.dart'; +import 'services/notification_service.dart'; +import 'services/bluetooth_service.dart'; +import 'services/location_service.dart'; +import 'services/alert_service.dart'; +import 'services/sms_service.dart'; +import 'services/video_recording_service.dart'; + +/// Global theme mode notifier β€” toggled from ProfileScreen. +final ValueNotifier themeNotifier = ValueNotifier(ThemeMode.dark); + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Load saved theme preference + try { + final prefs = await SharedPreferences.getInstance(); + final isDark = prefs.getBool('dark_mode') ?? true; + themeNotifier.value = isDark ? ThemeMode.dark : ThemeMode.light; + } catch (_) {} + + // Initialize Firebase + try { + await Firebase.initializeApp(); + } catch (_) {} + + // Notification setup + try { + await NotificationService.initialize(); + } catch (_) {} + + // Auto-connect to last saved Bluetooth device + try { + await BluetoothService.instance.tryAutoConnect(); + } catch (_) {} + + // Register global SOS listener (fires from any screen) + BluetoothService.instance.onCommand = (cmd) { + if (cmd == BraceletCommand.sos || cmd == BraceletCommand.shake) { + _triggerGlobalSOS(); + } + }; + + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + systemNavigationBarColor: Color(0xFF0A0A0F), + systemNavigationBarIconBrightness: Brightness.light, + ), + ); + runApp(const SheShieldApp()); +} + +/// Trigger SOS from any screen by pushing a full-screen alert. +void _triggerGlobalSOS() { + final nav = BluetoothService.navigatorKey.currentState; + if (nav == null) return; + + // Prevent duplicate SOS screens + // Fire all SOS services + try { LocationService.getCurrentLocation().then((pos) { + NotificationService.sendSOSAlert(lat: pos.latitude, lng: pos.longitude); + AlertService.broadcastAlert(lat: pos.latitude, lng: pos.longitude); + SmsService.sendSOSToAllContacts(lat: pos.latitude, lng: pos.longitude); + }); } catch (_) {} + + try { VideoRecordingService.startSOSRecording(); } catch (_) {} + try { LocationService.startSOS('user_placeholder'); } catch (_) {} + try { BluetoothService.instance.activateBuzzer(); } catch (_) {} + + HapticFeedback.heavyImpact(); + + nav.push( + PageRouteBuilder( + opaque: true, + pageBuilder: (_, __, ___) => SOSScreen( + onCancel: () { + try { LocationService.stopSOS(); } catch (_) {} + try { VideoRecordingService.stopAndUpload(); } catch (_) {} + try { BluetoothService.instance.stopBuzzer(); } catch (_) {} + nav.pop(); + }, + ), + transitionsBuilder: (_, anim, __, child) => + FadeTransition(opacity: anim, child: child), + transitionDuration: const Duration(milliseconds: 300), + ), + ); +} + +class SheShieldApp extends StatelessWidget { + const SheShieldApp({super.key}); + + // ──── DARK THEME ──── + static final _darkTheme = ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF0A0A0F), + colorScheme: const ColorScheme.dark( + primary: Color(0xFFE53935), + secondary: Color(0xFFE53935), + surface: Color(0xFF1A1A2E), + onSurface: Color(0xFFF0F0F5), + ), + fontFamily: 'Inter', + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFF0A0A0F), + elevation: 0, + centerTitle: false, + titleTextStyle: TextStyle( + fontFamily: 'Inter', + fontSize: 20, + fontWeight: FontWeight.w700, + color: Color(0xFFF0F0F5), + letterSpacing: -0.3, + ), + iconTheme: IconThemeData(color: Color(0xFFF0F0F5)), + ), + cardTheme: CardThemeData( + color: const Color(0xFF1A1A2E), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.white.withValues(alpha: 0.06)), + ), + elevation: 0, + margin: EdgeInsets.zero, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE53935), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + textStyle: const TextStyle( + fontFamily: 'Inter', fontSize: 15, fontWeight: FontWeight.w700), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xFF1A1A2E), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.06)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.06)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFFE53935)), + ), + labelStyle: const TextStyle( + color: Color(0xFF8A8A9A), fontSize: 12, fontWeight: FontWeight.w600), + hintStyle: const TextStyle(color: Color(0xFF5A5A6E), fontSize: 15), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ); + + // ──── LIGHT THEME ──── + static final _lightTheme = ThemeData( + useMaterial3: true, + brightness: Brightness.light, + scaffoldBackgroundColor: const Color(0xFFF5F5F8), + colorScheme: const ColorScheme.light( + primary: Color(0xFFE53935), + secondary: Color(0xFFE53935), + surface: Colors.white, + onSurface: Color(0xFF1A1A2E), + ), + fontFamily: 'Inter', + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFFF5F5F8), + elevation: 0, + centerTitle: false, + titleTextStyle: TextStyle( + fontFamily: 'Inter', + fontSize: 20, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A2E), + letterSpacing: -0.3, + ), + iconTheme: IconThemeData(color: Color(0xFF1A1A2E)), + ), + cardTheme: CardThemeData( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.black.withValues(alpha: 0.08)), + ), + elevation: 0, + margin: EdgeInsets.zero, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE53935), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + textStyle: const TextStyle( + fontFamily: 'Inter', fontSize: 15, fontWeight: FontWeight.w700), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.black.withValues(alpha: 0.08)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.black.withValues(alpha: 0.08)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFFE53935)), + ), + labelStyle: const TextStyle( + color: Color(0xFF5A5A6E), fontSize: 12, fontWeight: FontWeight.w600), + hintStyle: const TextStyle(color: Color(0xFF8A8A9A), fontSize: 15), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: themeNotifier, + builder: (_, mode, __) { + return MaterialApp( + title: 'She Shield', + debugShowCheckedModeBanner: false, + themeMode: mode, + theme: _lightTheme, + darkTheme: _darkTheme, + navigatorKey: BluetoothService.navigatorKey, + home: const _SplashGate(), + ); + }, + ); + } +} + +/// Shows the animated splash for at least 2.5 s, then transitions +/// to HomeScreen or LoginScreen based on Firebase auth state. +class _SplashGate extends StatefulWidget { + const _SplashGate(); + + @override + State<_SplashGate> createState() => _SplashGateState(); +} + +class _SplashGateState extends State<_SplashGate> { + bool _minTimeDone = false; + bool _authResolved = false; + bool? _isLoggedIn; + bool _securityChecked = false; + bool _securityDone = false; + + @override + void initState() { + super.initState(); + // Minimum splash duration + Future.delayed(const Duration(milliseconds: 2500), () { + if (mounted) setState(() => _minTimeDone = true); + }); + // Listen to auth + FirebaseAuth.instance.authStateChanges().first.then((user) { + if (mounted) { + setState(() { + _authResolved = true; + _isLoggedIn = user != null; + }); + // Check security setup status if logged in + if (user != null) _checkSecuritySetup(); + } + }); + } + + Future _checkSecuritySetup() async { + final done = await SecuritySetupScreen.isCompleted(); + if (mounted) { + setState(() { + _securityChecked = true; + _securityDone = done; + }); + // If not done, show the security setup screen + if (!done) { + final result = await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const SecuritySetupScreen()), + ); + if (result == true && mounted) { + setState(() => _securityDone = true); + } + } + } + } + + @override + Widget build(BuildContext context) { + if (_minTimeDone && _authResolved) { + if (_isLoggedIn == true) { + // Wait for security check to complete + if (!_securityChecked) return const _SplashScreen(); + if (!_securityDone) return const _SplashScreen(); + return const HomeScreen(); + } + return const LoginScreen(); + } + return const _SplashScreen(); + } +} + +// ══════════════════════════════════════════════════════════ +// ANIMATED SPLASH SCREEN (shown during Firebase init) +// ══════════════════════════════════════════════════════════ + +class _SplashScreen extends StatefulWidget { + const _SplashScreen(); + + @override + State<_SplashScreen> createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State<_SplashScreen> + with TickerProviderStateMixin { + late AnimationController _pulseCtrl; + late AnimationController _rotateCtrl; + late AnimationController _fadeCtrl; + late Animation _pulseAnim; + late Animation _fadeAnim; + + @override + void initState() { + super.initState(); + + // Pulse: logo breathes + _pulseCtrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1400), + )..repeat(reverse: true); + _pulseAnim = Tween(begin: 0.88, end: 1.0).animate( + CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut), + ); + + // Rotate: glow ring spins + _rotateCtrl = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..repeat(); + + // Fade: everything fades in + _fadeCtrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 900), + )..forward(); + _fadeAnim = CurvedAnimation(parent: _fadeCtrl, curve: Curves.easeOut); + } + + @override + void dispose() { + _pulseCtrl.dispose(); + _rotateCtrl.dispose(); + _fadeCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF0A0A0F), + body: Center( + child: FadeTransition( + opacity: _fadeAnim, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Logo with rotating glow ring ── + SizedBox( + width: 180, + height: 180, + child: Stack( + alignment: Alignment.center, + children: [ + // Rotating outer glow ring + AnimatedBuilder( + animation: _rotateCtrl, + builder: (_, __) => Transform.rotate( + angle: _rotateCtrl.value * 6.2832, // 2Ο€ + child: Container( + width: 170, + height: 170, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: SweepGradient( + colors: [ + const Color(0xFFE53935).withValues(alpha: 0.0), + const Color(0xFFE53935).withValues(alpha: 0.6), + const Color(0xFFFF7043).withValues(alpha: 0.3), + const Color(0xFFE53935).withValues(alpha: 0.0), + ], + ), + ), + ), + ), + ), + // Dark inner circle (mask) + Container( + width: 155, + height: 155, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFF0A0A0F), + ), + ), + // Pulsing logo + AnimatedBuilder( + animation: _pulseAnim, + builder: (_, __) => Transform.scale( + scale: _pulseAnim.value, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(60), + boxShadow: [ + BoxShadow( + color: const Color(0xFFE53935) + .withValues(alpha: 0.25 * _pulseAnim.value), + blurRadius: 50, + spreadRadius: 2, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(60), + child: Image.asset( + 'assets/images/logo.png', + fit: BoxFit.contain, + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 36), + // ── App name ── + RichText( + text: const TextSpan( + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.w800, + fontFamily: 'Inter', + letterSpacing: -0.5, + ), + children: [ + TextSpan( + text: 'She', + style: TextStyle(color: Color(0xFFF0F0F5)), + ), + TextSpan( + text: 'Shield', + style: TextStyle(color: Color(0xFFE53935)), + ), + ], + ), + ), + const SizedBox(height: 8), + const Text( + 'SMART SAFETY SYSTEM', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF5A5A6E), + letterSpacing: 3.0, + ), + ), + const SizedBox(height: 48), + // ── Shimmer loading bar ── + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: SizedBox( + width: 120, + height: 4, + child: AnimatedBuilder( + animation: _rotateCtrl, + builder: (_, __) { + return CustomPaint( + painter: _ShimmerBarPainter(_rotateCtrl.value), + ); + }, + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +/// Draws a small shimmer bar that slides across. +class _ShimmerBarPainter extends CustomPainter { + final double progress; + _ShimmerBarPainter(this.progress); + + @override + void paint(Canvas canvas, Size size) { + // Track + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + const Radius.circular(4), + ), + Paint()..color = const Color(0xFF1A1A2E), + ); + + // Shimmer + final shimmerWidth = size.width * 0.4; + final left = -shimmerWidth + (size.width + shimmerWidth) * progress; + final shimmerRect = Rect.fromLTWH(left, 0, shimmerWidth, size.height); + canvas.drawRRect( + RRect.fromRectAndRadius(shimmerRect, const Radius.circular(4)), + Paint() + ..shader = const LinearGradient( + colors: [ + Color(0x00E53935), + Color(0xFFE53935), + Color(0x00E53935), + ], + ).createShader(shimmerRect), + ); + } + + @override + bool shouldRepaint(_ShimmerBarPainter old) => old.progress != progress; +} diff --git a/Team-Shivam/SheShield/lib/screens/bluetooth_screen.dart b/Team-Shivam/SheShield/lib/screens/bluetooth_screen.dart new file mode 100644 index 0000000..c17dc03 --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/bluetooth_screen.dart @@ -0,0 +1,657 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bluetooth_serial_ble/flutter_bluetooth_serial_ble.dart'; +import 'package:permission_handler/permission_handler.dart'; +import '../services/bluetooth_service.dart'; + +class BluetoothScreen extends StatefulWidget { + const BluetoothScreen({super.key}); + @override + State createState() => _BluetoothScreenState(); +} + +class _BluetoothScreenState extends State { + final FlutterBluetoothSerial _bt = FlutterBluetoothSerial.instance; + final BluetoothService _service = BluetoothService.instance; + + List _scanResults = []; + List _bondedDevices = []; + bool _isScanning = false; + StreamSubscription? _discoverySub; + + @override + void initState() { + super.initState(); + _service.addListener(_onServiceChanged); + _init(); + } + + @override + void dispose() { + _discoverySub?.cancel(); + _service.removeListener(_onServiceChanged); + super.dispose(); + } + + void _onServiceChanged() { + if (mounted) setState(() {}); + } + + Future _init() async { + await _requestPermissions(); + await _loadBondedDevices(); + if (!_service.isConnected) _startScan(); + } + + Future _requestPermissions() async { + await [ + Permission.bluetooth, + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.location, + ].request(); + } + + Future _loadBondedDevices() async { + try { + final bonded = await _bt.getBondedDevices(); + if (mounted) setState(() => _bondedDevices = bonded); + } catch (_) {} + } + + Future _startScan() async { + if (_isScanning) return; + + // Check if Bluetooth is enabled + final isOn = await _bt.isEnabled ?? false; + if (!isOn) { + if (mounted) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + title: const Row( + children: [ + Icon(Icons.bluetooth_disabled, + color: Color(0xFFE53935), size: 24), + SizedBox(width: 10), + Text('Bluetooth Off', + style: TextStyle( + fontWeight: FontWeight.w700, fontSize: 18)), + ], + ), + content: const Text( + 'Please enable Bluetooth to connect to your SheShield device.', + style: TextStyle(fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + Navigator.pop(ctx); + await _bt.requestEnable(); + await Future.delayed(const Duration(seconds: 1)); + _startScan(); + }, + child: const Text('Enable', + style: TextStyle( + color: Color(0xFFE53935), + fontWeight: FontWeight.w700)), + ), + ], + ), + ); + } + return; + } + + setState(() { + _scanResults = []; + _isScanning = true; + }); + + try { + _discoverySub?.cancel(); + _discoverySub = _bt.startDiscovery().listen( + (result) { + if (!mounted) return; + setState(() { + final idx = _scanResults.indexWhere( + (r) => r.device.address == result.device.address); + if (idx >= 0) { + _scanResults[idx] = result; + } else { + _scanResults.add(result); + } + }); + }, + onDone: () { + if (mounted) setState(() => _isScanning = false); + }, + onError: (_) { + if (mounted) setState(() => _isScanning = false); + }, + ); + } catch (e) { + _snack('⚠️ Scan failed: $e'); + setState(() => _isScanning = false); + } + } + + void _stopScan() { + _discoverySub?.cancel(); + setState(() => _isScanning = false); + } + + Future _connectToDevice(BluetoothDevice device) async { + _stopScan(); + final name = device.name ?? device.address; + _snack('πŸ”— Connecting to $name…'); + + try { + if (device.isBonded != true) { + final bonded = await FlutterBluetoothSerial.instance + .bondDeviceAtAddress(device.address); + if (bonded != true) { + _popup('❌ Pairing Failed', 'Could not pair with $name.'); + return; + } + } + + final ok = await _service.connect(device); + if (ok && mounted) { + HapticFeedback.heavyImpact(); + _snack('βœ… Connected to $name'); + await _loadBondedDevices(); + } else { + _popup('❌ Failed', 'Could not connect to $name.'); + } + } catch (e) { + _popup('❌ Error', '$e'); + } + } + + void _disconnect() { + _service.disconnect(); + _snack('πŸ”΄ Disconnected'); + } + + void _popup(String title, String msg) { + if (!mounted) return; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text(title, + style: const TextStyle( + fontWeight: FontWeight.w700, fontSize: 18)), + content: + Text(msg, style: const TextStyle(fontSize: 14)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('OK', + style: TextStyle( + color: Color(0xFFE53935), + fontWeight: FontWeight.w700)), + ), + ], + ), + ); + } + + void _snack(String msg) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(msg, style: const TextStyle(fontWeight: FontWeight.w600)), + backgroundColor: Theme.of(context).colorScheme.surface, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: const EdgeInsets.all(16), + )); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Scaffold( + appBar: AppBar( + title: const Text('Bluetooth Pairing'), + leading: IconButton( + icon: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1A1A2E) : Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08)), + ), + child: const Icon(Icons.arrow_back, size: 18), + ), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + child: Column( + children: [ + // ── Connected card ── + if (_service.isConnected) _connectedCard(isDark), + const SizedBox(height: 8), + + // ── Scan button ── + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _isScanning ? _stopScan : _startScan, + icon: _isScanning + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, color: Colors.white)) + : const Icon(Icons.bluetooth_searching, size: 20), + label: + Text(_isScanning ? 'Stop Scanning' : 'Scan for Devices'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF42A5F5), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + elevation: 0, + ), + ), + ), + ), + const SizedBox(height: 16), + + // ── Paired devices ── + if (_bondedDevices.isNotEmpty) ...[ + _header('PAIRED DEVICES', _bondedDevices.length, isDark), + const SizedBox(height: 10), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: _bondedDevices.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (_, i) => + _deviceCard(_bondedDevices[i], null, isDark: isDark, isPaired: true), + ), + const SizedBox(height: 16), + ], + + // ── Discovered devices ── + _header('NEARBY DEVICES', _scanResults.length, isDark), + const SizedBox(height: 10), + if (_scanResults.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.bluetooth_disabled, + size: 48, + color: isDark + ? Colors.white.withValues(alpha: 0.15) + : Colors.black.withValues(alpha: 0.15)), + const SizedBox(height: 12), + Text( + _isScanning + ? 'Searching…' + : 'Tap "Scan" to find devices', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + ), + ), + ], + ), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: _scanResults.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (_, i) => _deviceCard( + _scanResults[i].device, _scanResults[i].rssi, + isDark: isDark), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + Widget _header(String title, int count, bool isDark) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Text(title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + letterSpacing: 0.5, + )), + const Spacer(), + Text('$count found', + style: TextStyle( + fontSize: 12, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + )), + ], + ), + ); + + Widget _connectedCard(bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF1B5E20), Color(0xFF1A1A2E)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFF00E676).withValues(alpha: 0.3)), + ), + child: Column( + children: [ + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0x2600E676), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Icon(Icons.bluetooth_connected, + color: Color(0xFF00E676), size: 24), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('BRACELET ACTIVE', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Color(0xFF00E676), + letterSpacing: 0.5, + )), + const SizedBox(height: 2), + Text(_service.deviceName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFFF0F0F5), + )), + const Text( + 'Connected globally β€” SOS active even after leaving this screen', + style: TextStyle( + fontSize: 10, color: Color(0xFF8A8A9A)), + ), + ], + ), + ), + GestureDetector( + onTap: _disconnect, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + decoration: BoxDecoration( + border: Border.all( + color: Colors.white.withValues(alpha: 0.12)), + borderRadius: BorderRadius.circular(10), + ), + child: const Text('Disconnect', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF8A8A9A), + )), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _SimBtn( + label: '🚨 Simulate SOS', + color: const Color(0xFFE53935), + onTap: () { + _service.simulateCommand(BraceletCommand.sos); + _snack('🚨 Simulated SOS'); + }, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _SimBtn( + label: 'πŸ“³ Simulate Shake', + color: const Color(0xFFFF7043), + onTap: () { + _service.simulateCommand(BraceletCommand.shake); + _snack('πŸ“³ Simulated shake'); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _deviceCard(BluetoothDevice device, int? rssi, + {required bool isDark, bool isPaired = false}) { + final isThis = + _service.isConnected && _service.deviceAddress == device.address; + final name = device.name?.isNotEmpty == true + ? device.name! + : 'Unknown (${device.address})'; + final isSheShield = + device.name?.toUpperCase().contains('SHESHIELD') == true; + final bg = isDark ? const Color(0xFF1A1A2E) : Colors.white; + + return Material( + color: bg, + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: isThis ? null : () => _connectToDevice(device), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isThis + ? const Color(0xFF00E676).withValues(alpha: 0.3) + : isSheShield + ? const Color(0xFFE53935).withValues(alpha: 0.3) + : isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08), + ), + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: isThis + ? const Color(0x2600E676) + : isSheShield + ? const Color(0x26E53935) + : const Color(0x1F42A5F5), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Icon( + isThis + ? Icons.bluetooth_connected + : isSheShield + ? Icons.watch + : Icons.bluetooth, + color: isThis + ? const Color(0xFF00E676) + : isSheShield + ? const Color(0xFFE53935) + : const Color(0xFF42A5F5), + size: 22, + ), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text(name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark + ? const Color(0xFFF0F0F5) + : const Color(0xFF1A1A2E), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ), + if (isSheShield) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFE53935) + .withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(6), + ), + child: const Text('SheShield', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: Color(0xFFE53935), + )), + ), + ], + ], + ), + const SizedBox(height: 2), + Text( + isThis + ? 'Connected' + : isPaired + ? 'Paired Β· Tap to connect' + : 'Tap to pair & connect', + style: TextStyle( + fontSize: 12, + color: isThis + ? const Color(0xFF00E676) + : isDark + ? const Color(0xFF8A8A9A) + : const Color(0xFF5A5A6E), + ), + ), + ], + ), + ), + if (rssi != null) ...[ + Icon(_sigIcon(rssi), + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + size: 20), + const SizedBox(width: 4), + Text('$rssi dBm', + style: TextStyle( + fontSize: 11, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + )), + ], + if (isPaired && rssi == null) + Icon(Icons.link, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + size: 18), + ], + ), + ), + ), + ); + } + + IconData _sigIcon(int r) { + if (r >= -60) return Icons.signal_cellular_4_bar; + if (r >= -75) return Icons.signal_cellular_alt; + if (r >= -85) return Icons.signal_cellular_alt_2_bar; + return Icons.signal_cellular_alt_1_bar; + } +} + +class _SimBtn extends StatelessWidget { + final String label; + final Color color; + final VoidCallback onTap; + const _SimBtn( + {required this.label, required this.color, required this.onTap}); + @override + Widget build(BuildContext context) => GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + border: Border.all(color: color.withValues(alpha: 0.4)), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text(label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: color)), + ), + ), + ); +} diff --git a/Team-Shivam/SheShield/lib/screens/contacts_screen.dart b/Team-Shivam/SheShield/lib/screens/contacts_screen.dart new file mode 100644 index 0000000..d38b8c5 --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/contacts_screen.dart @@ -0,0 +1,488 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../services/storage_service.dart'; +import '../services/sms_service.dart'; + +class ContactsScreen extends StatefulWidget { + const ContactsScreen({super.key}); + + @override + State createState() => _ContactsScreenState(); +} + +class _ContactsScreenState extends State { + List _contacts = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _loadContacts(); + } + + Future _loadContacts() async { + final contacts = await StorageService.loadContacts(); + if (mounted) { + setState(() { + _contacts = contacts; + _loading = false; + }); + } + } + + Future _confirmAndAddContact(String name, String phone) async { + // Show confirmation prompt before adding + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: const Color(0xFF1A1A2E), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: const Text( + 'πŸ“© Send Notification SMS?', + style: TextStyle( + color: Color(0xFFF0F0F5), + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + content: Text( + 'An SMS will be sent to $name ($phone) notifying them that they have been added as your emergency contact on SheShield.', + style: const TextStyle(color: Color(0xFF8A8A9A), fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text( + 'Cancel', + style: TextStyle(color: Color(0xFF8A8A9A)), + ), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text( + 'Add & Send SMS', + style: TextStyle( + color: Color(0xFFE53935), + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ); + + if (confirmed != true) return; + + // Save the contact + final updated = await StorageService.addContact(_contacts, name, phone); + setState(() => _contacts = updated); + + // Send SMS in the background + final smsSent = await SmsService.sendContactAddedSms( + phoneNumber: phone, + contactName: name, + ); + + if (smsSent) { + _showPopup('βœ… Contact Added', '$name has been added and an SMS notification has been sent.'); + } else { + _showPopup('βœ… Contact Added', '$name has been added but SMS could not be sent. Please check SMS permissions.'); + } + } + + Future _deleteContact(String id) async { + final updated = await StorageService.deleteContact(_contacts, id); + setState(() => _contacts = updated); + _showPopup('πŸ—‘οΈ Contact Removed', 'Contact has been removed from your list.'); + } + + void _sendSOSToAll() { + if (_contacts.isEmpty) { + _showPopup('⚠️ No Contacts', 'Please add emergency contacts first.'); + return; + } + HapticFeedback.heavyImpact(); + _showPopup( + '🚨 SOS Sent!', + 'Emergency alert has been sent to ${_contacts.length} contact${_contacts.length > 1 ? 's' : ''}.', + ); + } + + void _showPopup(String title, String message) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: const Color(0xFF1A1A2E), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text( + title, + style: const TextStyle( + color: Color(0xFFF0F0F5), + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + content: Text( + message, + style: const TextStyle(color: Color(0xFF8A8A9A), fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text( + 'OK', + style: TextStyle( + color: Color(0xFFE53935), + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ); + } + + void _showSnackBar(String msg) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(msg, style: const TextStyle(fontWeight: FontWeight.w600)), + backgroundColor: const Color(0xFF1A1A2E), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: const EdgeInsets.all(16), + duration: const Duration(seconds: 2), + ), + ); + } + + void _showAddContactSheet() { + final nameController = TextEditingController(); + final phoneController = TextEditingController(); + + showModalBottomSheet( + context: context, + backgroundColor: const Color(0xFF14141F), + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (ctx) { + return Padding( + padding: EdgeInsets.only( + left: 24, + right: 24, + top: 20, + bottom: MediaQuery.of(ctx).viewInsets.bottom + 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Handle + Center( + child: Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(20), + ), + ), + ), + const SizedBox(height: 20), + const Text( + 'Add Emergency Contact', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Color(0xFFF0F0F5), + letterSpacing: -0.3, + ), + ), + const SizedBox(height: 20), + // Name + const Text( + 'NAME', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF8A8A9A), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 6), + TextField( + controller: nameController, + style: const TextStyle(color: Color(0xFFF0F0F5), fontSize: 15), + decoration: const InputDecoration(hintText: 'e.g. Mom'), + textCapitalization: TextCapitalization.words, + ), + const SizedBox(height: 14), + // Phone + const Text( + 'PHONE NUMBER', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF8A8A9A), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 6), + TextField( + controller: phoneController, + style: const TextStyle(color: Color(0xFFF0F0F5), fontSize: 15), + decoration: + const InputDecoration(hintText: 'e.g. +91 98765 43210'), + keyboardType: TextInputType.phone, + ), + const SizedBox(height: 20), + // Buttons + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(ctx), + style: OutlinedButton.styleFrom( + side: BorderSide(color: Colors.white.withValues(alpha: 0.06)), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle( + color: Color(0xFF8A8A9A), + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: ElevatedButton( + onPressed: () { + final name = nameController.text.trim(); + final phone = phoneController.text.trim(); + if (name.isEmpty || phone.isEmpty) { + _showSnackBar('⚠️ Please fill in all fields'); + return; + } + Navigator.pop(ctx); + _confirmAndAddContact(name, phone); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text('Save Contact'), + ), + ), + ], + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Emergency Contacts'), + leading: IconButton( + icon: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFF1A1A2E), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.white.withValues(alpha: 0.06)), + ), + child: const Icon(Icons.arrow_back, size: 18), + ), + onPressed: () => Navigator.pop(context), + ), + ), + body: _loading + ? const Center(child: CircularProgressIndicator(color: Color(0xFFE53935))) + : Column( + children: [ + // Contact List + Expanded( + child: _contacts.isEmpty + ? _buildEmptyState() + : RefreshIndicator( + onRefresh: _loadContacts, + color: const Color(0xFFE53935), + backgroundColor: const Color(0xFF14141F), + child: ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), + itemCount: _contacts.length, + separatorBuilder: (_, __) => + const SizedBox(height: 10), + itemBuilder: (_, i) => + _buildContactCard(_contacts[i]), + ), + ), + ), + // Actions + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + // Add Contact + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _showAddContactSheet, + icon: const Text('οΌ‹', + style: TextStyle(fontSize: 18)), + label: const Text('Add New Contact'), + style: OutlinedButton.styleFrom( + side: BorderSide( + color: Colors.white.withValues(alpha: 0.12), + style: BorderStyle.solid, + ), + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + foregroundColor: const Color(0xFF8A8A9A), + ), + ), + ), + const SizedBox(height: 10), + // SOS All + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _sendSOSToAll, + icon: const Text('🚨', + style: TextStyle(fontSize: 18)), + label: const Text('Send SOS to All Contacts'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + ], + ), + ); + } + + Widget _buildEmptyState() { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('πŸ‘₯', style: TextStyle(fontSize: 48)), + SizedBox(height: 12), + Text( + 'No emergency contacts yet', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF5A5A6E), + ), + ), + ], + ), + ); + } + + Widget _buildContactCard(Contact contact) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: const Color(0xFF1A1A2E), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withValues(alpha: 0.06)), + ), + child: Row( + children: [ + // Avatar + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + const Color(0xFFE53935).withValues(alpha: 0.12), + const Color(0xFFE53935).withValues(alpha: 0.25), + ], + ), + ), + child: Center( + child: Text( + contact.name.isNotEmpty + ? contact.name[0].toUpperCase() + : '?', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Color(0xFFE53935), + ), + ), + ), + ), + const SizedBox(width: 14), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + contact.name, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFFF0F0F5), + ), + ), + const SizedBox(height: 2), + Text( + contact.phone, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF8A8A9A), + ), + ), + ], + ), + ), + // Delete Button + GestureDetector( + onTap: () => _deleteContact(contact.id), + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white.withValues(alpha: 0.06)), + ), + child: const Center( + child: Icon(Icons.close, size: 14, color: Color(0xFF5A5A6E)), + ), + ), + ), + ], + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/emergency_status_screen.dart b/Team-Shivam/SheShield/lib/screens/emergency_status_screen.dart new file mode 100644 index 0000000..46ddb0b --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/emergency_status_screen.dart @@ -0,0 +1,333 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; + +/// Live status screen shown after SOS is triggered. +/// +/// Displays real-time status of all emergency actions: +/// location sharing, contact alerts, video recording, etc. +class EmergencyStatusScreen extends StatefulWidget { + final VoidCallback onDismiss; + final double lat; + final double lng; + + const EmergencyStatusScreen({ + super.key, + required this.onDismiss, + required this.lat, + required this.lng, + }); + + @override + State createState() => _EmergencyStatusScreenState(); +} + +class _EmergencyStatusScreenState extends State + with SingleTickerProviderStateMixin { + late AnimationController _fadeController; + int _elapsedSeconds = 0; + Timer? _elapsedTimer; + + @override + void initState() { + super.initState(); + _fadeController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + )..forward(); + + // Track elapsed time since SOS + _elapsedTimer = Timer.periodic(const Duration(seconds: 1), (_) { + if (mounted) setState(() => _elapsedSeconds++); + }); + } + + @override + void dispose() { + _fadeController.dispose(); + _elapsedTimer?.cancel(); + super.dispose(); + } + + String _formatElapsed(int sec) { + final m = sec ~/ 60; + final s = sec % 60; + return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final cardBg = isDark ? const Color(0xFF1A1A2E) : Colors.white; + final border = isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08); + final subtitleColor = + isDark ? const Color(0xFF8A8A9A) : const Color(0xFF5A5A6E); + + return Scaffold( + appBar: AppBar( + title: const Text('Emergency Status'), + leading: IconButton( + icon: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1A1A2E) : Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: border), + ), + child: const Icon(Icons.arrow_back, size: 18), + ), + onPressed: () { + widget.onDismiss(); + Navigator.pop(context); + }, + ), + ), + body: FadeTransition( + opacity: _fadeController, + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // ── SOS Active header ── + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFE53935), Color(0xFFC62828)], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFFE53935).withValues(alpha: 0.3), + blurRadius: 20, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + children: [ + const Text('🚨', style: TextStyle(fontSize: 40)), + const SizedBox(height: 8), + const Text( + 'SOS ACTIVE', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w800, + color: Colors.white, + letterSpacing: 2, + ), + ), + const SizedBox(height: 6), + Text( + 'Emergency active for ${_formatElapsed(_elapsedSeconds)}', + style: TextStyle( + fontSize: 14, + color: Colors.white.withValues(alpha: 0.8), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + // ── Status cards ── + _StatusCard( + cardBg: cardBg, + border: border, + icon: Icons.sos_rounded, + iconColor: const Color(0xFFE53935), + title: 'SOS Alert Sent', + subtitle: 'Emergency contacts have been alerted', + status: 'Delivered', + statusColor: const Color(0xFF00E676), + subtitleColor: subtitleColor, + isDark: isDark, + ), + const SizedBox(height: 12), + _StatusCard( + cardBg: cardBg, + border: border, + icon: Icons.location_on_rounded, + iconColor: const Color(0xFF42A5F5), + title: 'Location Shared', + subtitle: + '${widget.lat.toStringAsFixed(4)}Β°N, ${widget.lng.toStringAsFixed(4)}Β°E', + status: 'Live', + statusColor: const Color(0xFF00E676), + subtitleColor: subtitleColor, + isDark: isDark, + ), + const SizedBox(height: 12), + _StatusCard( + cardBg: cardBg, + border: border, + icon: Icons.videocam_rounded, + iconColor: const Color(0xFFFF7043), + title: 'Video Recording', + subtitle: _elapsedSeconds < 30 + ? 'Recording in progress…' + : 'Recording completed & uploading', + status: _elapsedSeconds < 30 ? 'Recording' : 'Completed', + statusColor: _elapsedSeconds < 30 + ? const Color(0xFFFF9800) + : const Color(0xFF00E676), + subtitleColor: subtitleColor, + isDark: isDark, + ), + const SizedBox(height: 12), + _StatusCard( + cardBg: cardBg, + border: border, + icon: Icons.message_rounded, + iconColor: const Color(0xFF7C4DFF), + title: 'SMS Alerts', + subtitle: 'Emergency SMS with location sent', + status: 'Sent', + statusColor: const Color(0xFF00E676), + subtitleColor: subtitleColor, + isDark: isDark, + ), + const SizedBox(height: 12), + _StatusCard( + cardBg: cardBg, + border: border, + icon: Icons.cell_tower_rounded, + iconColor: const Color(0xFFFF9800), + title: 'Emergency Broadcast', + subtitle: 'Alert broadcasted to nearby devices', + status: 'Active', + statusColor: const Color(0xFF00E676), + subtitleColor: subtitleColor, + isDark: isDark, + ), + const SizedBox(height: 24), + // ── Stop SOS button ── + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () { + widget.onDismiss(); + Navigator.pop(context); + }, + icon: const Icon(Icons.close, size: 20), + label: const Text('Stop Emergency'), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFE53935), + side: const BorderSide( + color: Color(0xFFE53935), width: 1.5), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + textStyle: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ); + } +} + +/// Single status card row. +class _StatusCard extends StatelessWidget { + final Color cardBg; + final Color border; + final IconData icon; + final Color iconColor; + final String title; + final String subtitle; + final String status; + final Color statusColor; + final Color subtitleColor; + final bool isDark; + + const _StatusCard({ + required this.cardBg, + required this.border, + required this.icon, + required this.iconColor, + required this.title, + required this.subtitle, + required this.status, + required this.statusColor, + required this.subtitleColor, + required this.isDark, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: border), + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: iconColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: iconColor, size: 22), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark + ? const Color(0xFFF0F0F5) + : const Color(0xFF1A1A2E), + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: TextStyle( + fontSize: 12, + color: subtitleColor, + ), + ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + status, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: statusColor, + letterSpacing: 0.5, + ), + ), + ), + ], + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/fake_call_screen.dart b/Team-Shivam/SheShield/lib/screens/fake_call_screen.dart new file mode 100644 index 0000000..f081440 --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/fake_call_screen.dart @@ -0,0 +1,281 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Full-screen fake incoming call UI. +/// +/// Simulates a phone call for escape situations. +/// No real call is made β€” purely a visual/audio simulation. +class FakeCallScreen extends StatefulWidget { + const FakeCallScreen({super.key}); + + @override + State createState() => _FakeCallScreenState(); +} + +class _FakeCallScreenState extends State + with TickerProviderStateMixin { + late AnimationController _pulseController; + late AnimationController _slideController; + late Animation _pulseAnimation; + late Animation _slideAnimation; + Timer? _vibrateTimer; + + // Fake caller info β€” configurable + final String _callerName = 'Mom'; + final String _callerNumber = '+91 98765 43210'; + + @override + void initState() { + super.initState(); + + // Pulse animation for the call icon + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(reverse: true); + _pulseAnimation = Tween(begin: 1.0, end: 1.15).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + + // Slide animation for accept button + _slideController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(reverse: true); + _slideAnimation = Tween( + begin: const Offset(0, 0.15), + end: const Offset(0, -0.15), + ).animate(CurvedAnimation( + parent: _slideController, curve: Curves.easeInOut)); + + // Simulate vibration pattern (like a real call) + _vibrateTimer = Timer.periodic( + const Duration(milliseconds: 1000), + (_) => HapticFeedback.mediumImpact(), + ); + HapticFeedback.mediumImpact(); + } + + @override + void dispose() { + _pulseController.dispose(); + _slideController.dispose(); + _vibrateTimer?.cancel(); + super.dispose(); + } + + void _acceptCall() { + _vibrateTimer?.cancel(); + Navigator.pop(context); + // Could navigate to a "In Call" screen β€” for now just close + } + + void _declineCall() { + _vibrateTimer?.cancel(); + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + child: Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF1A1A2E), + Color(0xFF0A0A0F), + ], + ), + ), + child: SafeArea( + child: Column( + children: [ + const Spacer(flex: 2), + // ── Caller avatar ── + AnimatedBuilder( + animation: _pulseAnimation, + builder: (_, __) => Transform.scale( + scale: _pulseAnimation.value, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFF42A5F5).withValues(alpha: 0.15), + boxShadow: [ + BoxShadow( + color: const Color(0xFF42A5F5).withValues(alpha: 0.2), + blurRadius: 40, + spreadRadius: 10, + ), + ], + ), + child: const Center( + child: Icon( + Icons.person, + color: Color(0xFF42A5F5), + size: 56, + ), + ), + ), + ), + ), + const SizedBox(height: 32), + // ── Caller name ── + Text( + _callerName, + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.w700, + color: Color(0xFFF0F0F5), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Text( + _callerNumber, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: const Color(0xFFF0F0F5).withValues(alpha: 0.5), + letterSpacing: 1, + ), + ), + const SizedBox(height: 16), + // ── "Incoming call" label ── + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFF00E676).withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFF00E676), + ), + ), + const SizedBox(width: 8), + const Text( + 'Incoming Call', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF00E676), + letterSpacing: 0.5, + ), + ), + ], + ), + ), + const Spacer(flex: 3), + // ── Action buttons ── + Padding( + padding: const EdgeInsets.symmetric(horizontal: 48), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Decline + _CallButton( + icon: Icons.call_end, + color: const Color(0xFFE53935), + label: 'Decline', + onTap: _declineCall, + ), + // Accept + SlideTransition( + position: _slideAnimation, + child: _CallButton( + icon: Icons.call, + color: const Color(0xFF00E676), + label: 'Accept', + onTap: _acceptCall, + ), + ), + ], + ), + ), + const SizedBox(height: 48), + // ── Swipe hint ── + Text( + 'Tap to answer or decline', + style: TextStyle( + fontSize: 12, + color: const Color(0xFFF0F0F5).withValues(alpha: 0.3), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 32), + ], + ), + ), + ), + ), + ); + } +} + +/// Circular call action button. +class _CallButton extends StatelessWidget { + final IconData icon; + final Color color; + final String label; + final VoidCallback onTap; + + const _CallButton({ + required this.icon, + required this.color, + required this.label, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.4), + blurRadius: 20, + spreadRadius: 2, + ), + ], + ), + child: Icon(icon, color: Colors.white, size: 32), + ), + const SizedBox(height: 10), + Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: const Color(0xFFF0F0F5).withValues(alpha: 0.7), + ), + ), + ], + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/home_screen.dart b/Team-Shivam/SheShield/lib/screens/home_screen.dart new file mode 100644 index 0000000..0b66e53 --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/home_screen.dart @@ -0,0 +1,1709 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../services/location_service.dart'; +import '../services/notification_service.dart'; +import '../services/alert_service.dart'; +import '../services/voice_trigger_service.dart'; +import '../services/risk_assessment_service.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../services/sms_service.dart'; +import '../services/video_recording_service.dart'; +import '../services/bluetooth_service.dart'; +import 'location_screen.dart'; +import 'contacts_screen.dart'; +import 'nearby_police_screen.dart'; +import 'bluetooth_screen.dart'; +import 'past_emergencies_screen.dart'; +import 'profile_screen.dart'; +import 'sos_screen.dart'; +import 'fake_call_screen.dart'; +import 'emergency_status_screen.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State with TickerProviderStateMixin { + double _lat = 28.6139; + double _lng = 77.2090; + bool _sosActive = false; + bool _holding = false; + late AnimationController _pulseController; + late AnimationController _holdController; + late Animation _pulseAnimation; + VoiceTriggerService? _voiceService; + + /// Current risk assessment result β€” updated when location is fetched. + RiskResult? _riskResult; + + /// Auto SOS countdown state. + Timer? _autoSosTimer; + int _autoSosCountdown = 0; + bool _autoSosActive = false; + + final BluetoothService _bt = BluetoothService.instance; + + @override + void initState() { + super.initState(); + + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2000), + )..repeat(reverse: true); + + _pulseAnimation = Tween(begin: 1.0, end: 1.08).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + + _holdController = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + ); + _holdController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + setState(() => _holding = false); + _triggerSOS(); + } + }); + + // Listen to Bluetooth state changes + _bt.addListener(_onBtChanged); + + // Listen for bracelet commands globally + _bt.onCommand = (cmd) { + if (!mounted || _sosActive) return; + _triggerSOS(); + }; + + _fetchLocation(); + _initVoiceListener(); + _checkFirstLogin(); + } + + void _onBtChanged() { + if (mounted) setState(() {}); + } + + /// After first sign-in, prompt user to pair the bracelet. + Future _checkFirstLogin() async { + final prefs = await SharedPreferences.getInstance(); + final uid = FirebaseAuth.instance.currentUser?.uid; + if (uid == null) return; + final key = 'paired_$uid'; + if (prefs.getBool(key) == true) return; + + await Future.delayed(const Duration(milliseconds: 800)); + if (!mounted) return; + + final shouldPair = await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: const Text( + '⌚ Pair Your Device', + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 18), + ), + content: const Text( + 'Welcome to SheShield! Connect your smart safety device via Bluetooth to get started.', + style: TextStyle(fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text('Later', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5))), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Pair Now', + style: TextStyle( + color: Color(0xFFE53935), + fontWeight: FontWeight.w700)), + ), + ], + ), + ); + + await prefs.setBool(key, true); + + if (shouldPair == true && mounted) { + Navigator.push( + context, MaterialPageRoute(builder: (_) => const BluetoothScreen())); + } + } + + Future _initVoiceListener() async { + _voiceService = VoiceTriggerService(onSosDetected: () { + if (mounted && !_sosActive) _triggerSOS(); + }); + final available = await _voiceService!.initialize(); + if (available) { + _voiceService!.startListening(); + } + } + + @override + void dispose() { + _voiceService?.dispose(); + _pulseController.dispose(); + _holdController.dispose(); + _autoSosTimer?.cancel(); + _bt.removeListener(_onBtChanged); + super.dispose(); + } + + Future _fetchLocation() async { + try { + final pos = await LocationService.getCurrentLocation(); + if (mounted) { + setState(() { + _lat = pos.latitude; + _lng = pos.longitude; + }); + } + } catch (_) {} + // Assess risk after location is available (or with defaults) + _assessRisk(); + } + + /// Re-evaluate risk factors using current lat/lng and system time. + /// Shows instant offline result first, then upgrades to online if available. + void _assessRisk() { + // Instant offline result (no waiting) + final offlineResult = RiskAssessmentService.assessRisk(_lat, _lng); + if (mounted) { + setState(() => _riskResult = offlineResult); + _checkAutoSos(offlineResult); + } + + // Async online-enhanced upgrade (non-blocking) + RiskAssessmentService.assessRiskAsync(_lat, _lng).then((onlineResult) { + if (mounted) { + setState(() => _riskResult = onlineResult); + _checkAutoSos(onlineResult); + } + }); + } + + /// Start auto SOS countdown only when riskScore >= 2. + void _checkAutoSos(RiskResult result) { + if (result.riskScore >= 2 && !_autoSosActive && !_sosActive) { + _startAutoSosCountdown(); + } else if (result.riskScore < 2 && _autoSosActive) { + _cancelAutoSos(); + } + } + + /// Start a 5-second countdown β€” triggers SOS if not cancelled. + void _startAutoSosCountdown() { + if (_autoSosActive || _sosActive) return; + _autoSosTimer?.cancel(); + setState(() { + _autoSosActive = true; + _autoSosCountdown = 5; + }); + HapticFeedback.heavyImpact(); + _autoSosTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) { timer.cancel(); return; } + setState(() => _autoSosCountdown--); + HapticFeedback.mediumImpact(); + if (_autoSosCountdown <= 0) { + timer.cancel(); + _autoSosActive = false; + _triggerSOS(); + } + }); + } + + /// Cancel the auto SOS countdown. + void _cancelAutoSos() { + _autoSosTimer?.cancel(); + if (mounted) { + setState(() { + _autoSosActive = false; + _autoSosCountdown = 0; + }); + _showSnackBar('Auto SOS cancelled'); + } + } + + /// Open Google Maps with directions to the nearest safe zone. + Future _navigateToSafeZone() async { + final zone = _riskResult?.nearestSafeZone; + if (zone == null) return; + final url = Uri.parse( + 'https://www.google.com/maps/dir/?api=1' + '&origin=$_lat,$_lng' + '&destination=${zone.lat},${zone.lng}' + '&travelmode=driving', + ); + try { + await launchUrl(url, mode: LaunchMode.externalApplication); + } catch (_) { + _showSnackBar('Could not open Google Maps'); + } + } + + void _triggerSOS() { + if (_sosActive) return; + HapticFeedback.heavyImpact(); + setState(() => _sosActive = true); + + // Fire all SOS services + NotificationService.sendSOSAlert(lat: _lat, lng: _lng); + AlertService.broadcastAlert(lat: _lat, lng: _lng); + SmsService.sendSOSToAllContacts(lat: _lat, lng: _lng); + VideoRecordingService.startSOSRecording(); + LocationService.startSOS('user_placeholder'); + + // Activate buzzer on ESP32 + _bt.activateBuzzer(); + + // Navigate to full-screen SOS alert + Navigator.of(context).push( + PageRouteBuilder( + opaque: true, + pageBuilder: (_, __, ___) => SOSScreen( + onCancel: () { + _stopAllSosServices(); + Navigator.of(context).pop(); // pop SOS screen back to home + _showSnackBar('βšͺ SOS alert cancelled'); + _voiceService?.resetAndRestart(); + }, + ), + transitionsBuilder: (_, anim, __, child) => + FadeTransition(opacity: anim, child: child), + transitionDuration: const Duration(milliseconds: 300), + ), + ); + + // After 3 seconds, REPLACE the SOS screen with Emergency Status + Future.delayed(const Duration(seconds: 3), () { + if (!mounted || !_sosActive) return; + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => EmergencyStatusScreen( + lat: _lat, + lng: _lng, + onDismiss: () { + _stopAllSosServices(); + _voiceService?.resetAndRestart(); + }, + ), + ), + ); + }); + } + + /// Shared cleanup for stopping all SOS services. + void _stopAllSosServices() { + LocationService.stopSOS(); + VideoRecordingService.stopAndUpload(); + _bt.stopBuzzer(); + setState(() => _sosActive = false); + } + + void _showSnackBar(String msg) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(msg, style: const TextStyle(fontWeight: FontWeight.w600)), + backgroundColor: Theme.of(context).colorScheme.surface, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: const EdgeInsets.all(16), + duration: const Duration(seconds: 2), + ), + ); + } + + Future _handleConnectTap() async { + if (_bt.isConnected) { + _bt.disconnect(); + _showSnackBar('πŸ”΄ Disconnected from ${_bt.deviceName}'); + } else if (_bt.deviceAddress.isNotEmpty) { + // Has a saved device β€” try reconnecting + _showSnackBar('πŸ”— Reconnecting to ${_bt.deviceName}…'); + final ok = await _bt.manualReconnect(); + if (ok && mounted) { + _showSnackBar('βœ… Connected to ${_bt.deviceName}'); + } else if (mounted) { + _showSnackBar('❌ Failed to connect. Open Bluetooth Pairing.'); + } + } else { + // No saved device β€” go to pairing + Navigator.push( + context, MaterialPageRoute(builder: (_) => const BluetoothScreen())); + } + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + _buildTopBar(isDark), + const SizedBox(height: 4), + _buildStatusCard(isDark), + const SizedBox(height: 10), + // ── Smart Risk Alert Card ── + _buildRiskAlertCard(isDark), + const SizedBox(height: 8), + _buildSOSSection(), + const SizedBox(height: 4), + _buildSubtitle(), + const SizedBox(height: 16), + _buildInfoCards(isDark), + const SizedBox(height: 16), + _buildNavButtons(isDark), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + // ════════════════════════════════════════════════════════ + // TOP BAR + // ════════════════════════════════════════════════════════ + + Widget _buildTopBar(bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Logo + title + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(11), + child: Image.asset( + 'assets/images/logo.png', + width: 38, + height: 38, + fit: BoxFit.contain, + ), + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: const TextSpan( + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w800, + fontFamily: 'Inter', + letterSpacing: -0.5, + ), + children: [ + TextSpan( + text: 'She', + style: TextStyle(color: Color(0xFFF0F0F5)), + ), + TextSpan( + text: 'Shield', + style: TextStyle(color: Color(0xFFE53935)), + ), + ], + ), + ), + Text( + 'Safety', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + letterSpacing: 2, + ), + ), + ], + ), + ], + ), + // Profile button + GestureDetector( + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ProfileScreen()), + ), + child: Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: isDark + ? const Color(0xFF1A1A2E) + : Colors.white, + borderRadius: BorderRadius.circular(11), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08)), + boxShadow: isDark + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon(Icons.person_outline, + color: isDark + ? const Color(0xFF8A8A9A) + : const Color(0xFF5A5A6E), + size: 20), + ), + ), + ], + ), + ); + } + + // ════════════════════════════════════════════════════════ + // STATUS CARD (real Bluetooth state) + // ════════════════════════════════════════════════════════ + + Widget _buildStatusCard(bool isDark) { + final connected = _bt.isConnected; + final connecting = _bt.isConnecting; + + final Color statusColor = connecting + ? const Color(0xFFFF9800) + : connected + ? const Color(0xFF00E676) + : const Color(0xFFE53935); + + final String statusText = connecting + ? 'CONNECTING…' + : connected + ? 'CONNECTED' + : 'DISCONNECTED'; + + final String statusSubtext = connecting + ? 'Establishing connection to ${_bt.deviceName}…' + : connected + ? '${_bt.deviceName} β€’ SOS signals active' + : _bt.deviceAddress.isNotEmpty + ? 'Tap to reconnect to ${_bt.deviceName}' + : 'No device paired. Tap to connect.'; + + final Color cardBg = isDark ? const Color(0xFF1A1A2E) : Colors.white; + final Color borderColor = statusColor.withValues(alpha: 0.25); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: GestureDetector( + onTap: _handleConnectTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: borderColor, width: 1.5), + boxShadow: [ + BoxShadow( + color: statusColor.withValues(alpha: 0.08), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + // Status dot + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: 44, + height: 44, + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: connecting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: statusColor, + ), + ) + : _StatusDot(connected: connected), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + statusText, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + color: statusColor, + letterSpacing: 0.8, + ), + ), + if (connected) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: + const Color(0xFF00E676).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + ), + child: const Text( + 'LIVE', + style: TextStyle( + fontSize: 8, + fontWeight: FontWeight.w800, + color: Color(0xFF00E676), + letterSpacing: 1, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 3), + Text( + statusSubtext, + style: TextStyle( + fontSize: 12, + color: isDark + ? const Color(0xFF8A8A9A) + : const Color(0xFF5A5A6E), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // Action icon + Icon( + connected + ? Icons.bluetooth_connected + : Icons.bluetooth_disabled, + color: statusColor.withValues(alpha: 0.6), + size: 22, + ), + ], + ), + ), + ), + ); + } + + // ════════════════════════════════════════════════════════ + // SOS BUTTON + // ════════════════════════════════════════════════════════ + + Widget _buildSOSSection() { + return Center( + child: AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return Stack( + alignment: Alignment.center, + children: [ + // Pulse rings + for (int i = 0; i < 3; i++) + Transform.scale( + scale: _pulseAnimation.value + (i * 0.04), + child: Container( + width: 200 + (i * 34.0), + height: 200 + (i * 34.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFFE53935) + .withValues(alpha: 0.15 - (i * 0.04)), + width: 2, + ), + ), + ), + ), + // SOS Button + GestureDetector( + onLongPressStart: (_) { + if (_sosActive) return; + HapticFeedback.mediumImpact(); + setState(() => _holding = true); + _holdController.forward(from: 0.0); + }, + onLongPressEnd: (_) { + if (_holdController.isAnimating) { + _holdController.reset(); + setState(() => _holding = false); + } + }, + child: Transform.scale( + scale: _sosActive ? 1.0 : _pulseAnimation.value * 0.97, + child: SizedBox( + width: 184, + height: 184, + child: Stack( + alignment: Alignment.center, + children: [ + // Hold progress ring + if (_holding) + AnimatedBuilder( + animation: _holdController, + builder: (context, _) { + return SizedBox( + width: 184, + height: 184, + child: CircularProgressIndicator( + value: _holdController.value, + strokeWidth: 5, + color: Colors.white, + backgroundColor: + Colors.white.withValues(alpha: 0.15), + ), + ); + }, + ), + // Main button + Container( + width: 170, + height: 170, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + center: const Alignment(-0.3, -0.3), + colors: _holding + ? [ + const Color(0xFFFF8A80), + const Color(0xFFFF5252), + const Color(0xFFE53935) + ] + : [ + const Color(0xFFFF5252), + const Color(0xFFE53935), + const Color(0xFFC62828) + ], + ), + boxShadow: [ + BoxShadow( + color: const Color(0xFFE53935).withValues( + alpha: _holding ? 0.55 : 0.35), + blurRadius: _holding ? 80 : 60, + ), + BoxShadow( + color: const Color(0xFFE53935) + .withValues(alpha: 0.15), + blurRadius: 120, + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('🚨', + style: TextStyle(fontSize: 28)), + const SizedBox(height: 2), + const Text( + 'SOS', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w800, + color: Colors.white, + letterSpacing: 4, + ), + ), + const SizedBox(height: 2), + Text( + _holding ? 'KEEP HOLDING…' : 'HOLD FOR HELP', + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: Colors.white70, + letterSpacing: 1.5, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ); + }, + ), + ); + } + + Widget _buildSubtitle() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Text( + 'Press and hold wearable button or shake device', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).brightness == Brightness.dark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + letterSpacing: 0.3, + ), + ), + ); + } + + // ════════════════════════════════════════════════════════ + // SMART RISK ALERT CARD + // ════════════════════════════════════════════════════════ + + Widget _buildRiskAlertCard(bool isDark) { + final risk = _riskResult; + + // While risk hasn't been evaluated yet, show a compact loading state + if (risk == null) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1A1A2E) : Colors.white, + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08), + ), + ), + child: Row( + children: [ + const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(width: 12), + Text( + 'Assessing your safety…', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: isDark + ? const Color(0xFF8A8A9A) + : const Color(0xFF5A5A6E), + ), + ), + ], + ), + ), + ); + } + + // ── Determine colours & icons based on risk level ── + late final Color accentColor; + late final Color bgGradientStart; + late final Color bgGradientEnd; + late final IconData iconData; + + switch (risk.level) { + case RiskLevel.critical: + accentColor = const Color(0xFFB71C1C); + bgGradientStart = const Color(0xFFB71C1C).withValues(alpha: 0.18); + bgGradientEnd = const Color(0xFFB71C1C).withValues(alpha: 0.06); + iconData = Icons.crisis_alert_rounded; + break; + case RiskLevel.high: + accentColor = const Color(0xFFE53935); + bgGradientStart = const Color(0xFFE53935).withValues(alpha: 0.15); + bgGradientEnd = const Color(0xFFE53935).withValues(alpha: 0.05); + iconData = Icons.warning_amber_rounded; + break; + case RiskLevel.moderate: + accentColor = const Color(0xFFFF9800); + bgGradientStart = const Color(0xFFFF9800).withValues(alpha: 0.15); + bgGradientEnd = const Color(0xFFFF9800).withValues(alpha: 0.05); + iconData = Icons.shield_outlined; + break; + case RiskLevel.safe: + accentColor = const Color(0xFF00E676); + bgGradientStart = const Color(0xFF00E676).withValues(alpha: 0.12); + bgGradientEnd = const Color(0xFF00E676).withValues(alpha: 0.03); + iconData = Icons.verified_user_rounded; + break; + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: Container( + key: ValueKey(risk.level), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [bgGradientStart, bgGradientEnd], + ), + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: accentColor.withValues(alpha: 0.35), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: accentColor.withValues(alpha: 0.10), + blurRadius: 24, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Header row ── + Row( + children: [ + // Icon container + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: accentColor.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(iconData, color: accentColor, size: 22), + ), + const SizedBox(width: 12), + // Title + subtitle + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'RISK ALERT', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w800, + color: accentColor, + letterSpacing: 1.0, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: accentColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + risk.level == RiskLevel.critical + ? 'CRITICAL' + : risk.level == RiskLevel.high + ? 'HIGH' + : risk.level == RiskLevel.moderate + ? 'MODERATE' + : 'SAFE', + style: TextStyle( + fontSize: 8, + fontWeight: FontWeight.w800, + color: accentColor, + letterSpacing: 1, + ), + ), + ), + const SizedBox(width: 6), + // Online/Offline mode badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, vertical: 2), + decoration: BoxDecoration( + color: risk.mode == DetectionMode.online + ? const Color(0xFF42A5F5).withValues(alpha: 0.15) + : Colors.grey.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + risk.mode == DetectionMode.online + ? Icons.wifi + : Icons.wifi_off, + size: 8, + color: risk.mode == DetectionMode.online + ? const Color(0xFF42A5F5) + : Colors.grey, + ), + const SizedBox(width: 3), + Text( + risk.mode == DetectionMode.online + ? 'ONLINE' + : 'OFFLINE', + style: TextStyle( + fontSize: 7, + fontWeight: FontWeight.w800, + color: risk.mode == DetectionMode.online + ? const Color(0xFF42A5F5) + : Colors.grey, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 3), + Text( + risk.message, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark + ? const Color(0xFFF0F0F5) + : const Color(0xFF1A1A2E), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 10), + // ── Subtitle / reason ── + Text( + risk.subtitle, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: isDark + ? const Color(0xFF8A8A9A) + : const Color(0xFF5A5A6E), + ), + ), + // ── Nearest safe zone info ── + if (risk.nearestSafeZone != null) ...[ + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.04), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.06), + ), + ), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: accentColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Text('πŸ“', style: TextStyle(fontSize: 16)), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + risk.level != RiskLevel.safe + ? 'NEAREST SAFE ZONE' + : 'NEARBY SAFE ZONE', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + letterSpacing: 0.8, + ), + ), + const SizedBox(height: 2), + Text( + '${risk.nearestSafeZone!.name} β€’ ${risk.nearestSafeZone!.type}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: isDark + ? const Color(0xFFF0F0F5) + : const Color(0xFF1A1A2E), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: accentColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${risk.nearestSafeZone!.distanceKm.toStringAsFixed(1)} km', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: accentColor, + ), + ), + ), + ], + ), + ), + ], + // ── Reason chips ── + if (risk.reasons.isNotEmpty) ...[ + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 6, + children: risk.reasons.map((r) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: accentColor.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + r, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: accentColor, + ), + ), + ); + }).toList(), + ), + ], + // ── Demo toggle: simulate isolated area ── + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: isDark + ? Colors.white.withValues(alpha: 0.04) + : Colors.black.withValues(alpha: 0.03), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Icon(Icons.science_outlined, + size: 16, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A)), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Simulate isolated area', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: isDark + ? const Color(0xFF8A8A9A) + : const Color(0xFF5A5A6E), + ), + ), + ), + SizedBox( + height: 28, + child: Switch( + value: RiskAssessmentService.isIsolatedArea, + activeThumbColor: accentColor, + onChanged: (val) { + setState(() { + RiskAssessmentService.isIsolatedArea = val; + }); + _assessRisk(); + }, + ), + ), + ], + ), + ), + // ── Auto SOS Countdown Banner ── + if (_autoSosActive) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFE53935).withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: const Color(0xFFE53935).withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + // Countdown number + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: const Color(0xFFE53935), + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Text( + '$_autoSosCountdown', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(width: 12), + // Text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '⚠️ Auto SOS in $_autoSosCountdown seconds', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: Color(0xFFE53935), + ), + ), + const SizedBox(height: 2), + Text( + 'High risk detected', + style: TextStyle( + fontSize: 11, + color: isDark + ? const Color(0xFF8A8A9A) + : const Color(0xFF5A5A6E), + ), + ), + ], + ), + ), + // Cancel button + GestureDetector( + onTap: _cancelAutoSos, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: isDark + ? Colors.white.withValues(alpha: 0.08) + : Colors.black.withValues(alpha: 0.06), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + 'Cancel', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: isDark + ? const Color(0xFFF0F0F5) + : const Color(0xFF1A1A2E), + ), + ), + ), + ), + ], + ), + ), + ], + // ── Action buttons (SOS + Navigate) when risk detected ── + if (risk.level != RiskLevel.safe) ...[ + const SizedBox(height: 14), + Row( + children: [ + // Trigger SOS button + Expanded( + child: ElevatedButton.icon( + onPressed: _sosActive ? null : _triggerSOS, + icon: const Text('🚨', style: TextStyle(fontSize: 14)), + label: const Text( + 'Trigger SOS', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: accentColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + ), + ), + // Navigate to safe zone button + if (risk.nearestSafeZone != null) ...[ + const SizedBox(width: 10), + Expanded( + child: ElevatedButton.icon( + onPressed: _navigateToSafeZone, + icon: const Icon(Icons.navigation_rounded, size: 18), + label: const Text( + 'Navigate', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF42A5F5), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + ), + ), + ], + ], + ), + ], + ], + ), + ), + ), + ); + } + + // ════════════════════════════════════════════════════════ + // INFO CARDS + // ════════════════════════════════════════════════════════ + + Widget _buildInfoCards(bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + _InfoCard( + isDark: isDark, + icon: 'πŸ“', + iconBgColor: const Color(0x1F42A5F5), + label: 'CURRENT LOCATION', + value: + '${_lat.toStringAsFixed(4)}Β°N, ${_lng.toStringAsFixed(4)}Β°E', + trailing: Icon(Icons.chevron_right, + color: + isDark ? const Color(0xFF5A5A6E) : const Color(0xFF8A8A9A), + size: 20), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => LocationScreen(lat: _lat, lng: _lng)), + ), + ), + const SizedBox(height: 12), + _InfoCard( + isDark: isDark, + icon: '⌚', + iconBgColor: const Color(0x2600E676), + label: 'BRACELET STATUS', + value: _bt.isConnected ? 'Active β€’ SOS Ready' : 'Not Connected', + ), + ], + ), + ); + } + + // ════════════════════════════════════════════════════════ + // NAV BUTTONS + // ════════════════════════════════════════════════════════ + + Widget _buildNavButtons(bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _NavButton( + isDark: isDark, + icon: 'πŸ—ΊοΈ', + iconBgColor: const Color(0x1F42A5F5), + label: 'Live\nLocation', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + LocationScreen(lat: _lat, lng: _lng)), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _NavButton( + isDark: isDark, + icon: 'πŸš”', + iconBgColor: const Color(0x1F42A5F5), + label: 'Police\nStations', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const NearbyPoliceScreen()), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _NavButton( + isDark: isDark, + icon: 'πŸ‘₯', + iconBgColor: const Color(0x1FE53935), + label: 'Emergency\nContacts', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const ContactsScreen()), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _NavButton( + isDark: isDark, + icon: 'πŸ“‘', + iconBgColor: const Color(0x1F7C4DFF), + label: 'Bluetooth\nPairing', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const BluetoothScreen()), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _NavButton( + isDark: isDark, + icon: 'πŸ“Ή', + iconBgColor: const Color(0x1FFF7043), + label: 'Past\nEmergencies', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const PastEmergenciesScreen()), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _NavButton( + isDark: isDark, + icon: 'πŸ“ž', + iconBgColor: const Color(0x1F00E676), + label: 'Fake\nCall', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const FakeCallScreen()), + ), + ), + ), + ], + ), + ], + ), + ); + } +} + +// ============================================ +// Sub-widgets +// ============================================ + +class _StatusDot extends StatefulWidget { + final bool connected; + const _StatusDot({required this.connected}); + + @override + State<_StatusDot> createState() => _StatusDotState(); +} + +class _StatusDotState extends State<_StatusDot> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final color = + widget.connected ? const Color(0xFF00E676) : const Color(0xFFE53935); + return SizedBox( + width: 18, + height: 18, + child: Stack( + alignment: Alignment.center, + children: [ + if (widget.connected) + AnimatedBuilder( + animation: _controller, + builder: (_, __) { + return Container( + width: 18, + height: 18, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: color.withValues(alpha: 1.0 - _controller.value), + width: 2, + ), + ), + ); + }, + ), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.5), + blurRadius: 6, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _InfoCard extends StatelessWidget { + final bool isDark; + final String icon; + final Color iconBgColor; + final String label; + final String value; + final Widget? trailing; + final VoidCallback? onTap; + + const _InfoCard({ + required this.isDark, + required this.icon, + required this.iconBgColor, + required this.label, + required this.value, + this.trailing, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bg = isDark ? const Color(0xFF1A1A2E) : Colors.white; + final border = isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08); + + return Material( + color: bg, + borderRadius: BorderRadius.circular(16), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: border), + ), + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text(icon, + style: const TextStyle(fontSize: 20))), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 3), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark + ? const Color(0xFFF0F0F5) + : const Color(0xFF1A1A2E), + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + if (trailing != null) trailing!, + ], + ), + ), + ), + ); + } +} + +class _NavButton extends StatelessWidget { + final bool isDark; + final String icon; + final Color iconBgColor; + final String label; + final VoidCallback onTap; + + const _NavButton({ + required this.isDark, + required this.icon, + required this.iconBgColor, + required this.label, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bg = isDark ? const Color(0xFF1A1A2E) : Colors.white; + final border = isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08); + + return Material( + color: bg, + borderRadius: BorderRadius.circular(16), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: border), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text(icon, + style: const TextStyle(fontSize: 24))), + ), + const SizedBox(height: 10), + Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isDark + ? const Color(0xFFF0F0F5) + : const Color(0xFF1A1A2E), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/location_screen.dart b/Team-Shivam/SheShield/lib/screens/location_screen.dart new file mode 100644 index 0000000..0862cf6 --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/location_screen.dart @@ -0,0 +1,389 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../services/location_service.dart'; + +class LocationScreen extends StatefulWidget { + final double lat; + final double lng; + + const LocationScreen({super.key, required this.lat, required this.lng}); + + @override + State createState() => _LocationScreenState(); +} + +class _LocationScreenState extends State { + late double _lat; + late double _lng; + GoogleMapController? _mapController; + StreamSubscription? _locationSub; + final Set _markers = {}; + + @override + void initState() { + super.initState(); + _lat = widget.lat; + _lng = widget.lng; + _updateMarker(); + _startLocationUpdates(); + } + + @override + void dispose() { + _locationSub?.cancel(); + _mapController?.dispose(); + super.dispose(); + } + + void _updateMarker() { + _markers.clear(); + _markers.add( + Marker( + markerId: const MarkerId('user_location'), + position: LatLng(_lat, _lng), + icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed), + infoWindow: const InfoWindow(title: 'Your Location'), + ), + ); + } + + void _startLocationUpdates() { + _locationSub = LocationService.getLocationStream().listen( + (pos) { + if (mounted) { + setState(() { + _lat = pos.latitude; + _lng = pos.longitude; + _updateMarker(); + }); + _mapController?.animateCamera( + CameraUpdate.newLatLng(LatLng(_lat, _lng)), + ); + } + }, + onError: (_) {}, + ); + } + + void _shareLocation() { + final url = 'https://maps.google.com/?q=$_lat,$_lng'; + final text = 'πŸ“ She Shield – My Live Location\n' + 'Lat: ${_lat.toStringAsFixed(4)}Β°, Lng: ${_lng.toStringAsFixed(4)}Β°\n' + '$url'; + + showModalBottomSheet( + context: context, + backgroundColor: const Color(0xFF14141F), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (ctx) { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle + Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(20), + ), + ), + const SizedBox(height: 20), + const Text( + 'Share Location via', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Color(0xFFF0F0F5), + ), + ), + const SizedBox(height: 20), + // WhatsApp + _ShareOption( + icon: 'πŸ’¬', + label: 'WhatsApp', + color: const Color(0xFF25D366), + onTap: () async { + Navigator.pop(ctx); + final waUrl = 'https://wa.me/?text=${Uri.encodeComponent(text)}'; + try { + await launchUrl(Uri.parse(waUrl), mode: LaunchMode.externalApplication); + } catch (_) { + if (mounted) _showFallback(text); + } + }, + ), + const SizedBox(height: 10), + // SMS + _ShareOption( + icon: 'βœ‰οΈ', + label: 'Text Message (SMS)', + color: const Color(0xFF42A5F5), + onTap: () async { + Navigator.pop(ctx); + final smsUrl = 'sms:?body=${Uri.encodeComponent(text)}'; + try { + await launchUrl(Uri.parse(smsUrl)); + } catch (_) { + if (mounted) _showFallback(text); + } + }, + ), + const SizedBox(height: 10), + // Copy + _ShareOption( + icon: 'πŸ“‹', + label: 'Copy to Clipboard', + color: const Color(0xFF8A8A9A), + onTap: () async { + Navigator.pop(ctx); + await Clipboard.setData(ClipboardData(text: text)); + if (mounted) { + _showConfirmDialog( + 'πŸ“‹ Copied!', + 'Location link copied to clipboard. Paste it in any app.', + ); + } + }, + ), + ], + ), + ); + }, + ); + } + + void _showFallback(String text) async { + await Clipboard.setData(ClipboardData(text: text)); + if (mounted) { + _showConfirmDialog( + 'πŸ“‹ Copied Instead', + 'Could not open the app. Location link has been copied to clipboard.', + ); + } + } + + void _showConfirmDialog(String title, String message) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: const Color(0xFF1A1A2E), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text( + title, + style: const TextStyle( + color: Color(0xFFF0F0F5), + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + content: Text( + message, + style: const TextStyle(color: Color(0xFF8A8A9A), fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text( + 'OK', + style: TextStyle(color: Color(0xFFE53935), fontWeight: FontWeight.w700), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Live Location'), + leading: IconButton( + icon: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFF1A1A2E), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.white.withValues(alpha: 0.06)), + ), + child: const Icon(Icons.arrow_back, size: 18), + ), + onPressed: () => Navigator.pop(context), + ), + ), + body: Column( + children: [ + // Map + Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + height: 360, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withValues(alpha: 0.06)), + ), + clipBehavior: Clip.antiAlias, + child: GoogleMap( + initialCameraPosition: CameraPosition( + target: LatLng(_lat, _lng), + zoom: 15, + ), + markers: _markers, + onMapCreated: (controller) => _mapController = controller, + myLocationEnabled: true, + myLocationButtonEnabled: false, + zoomControlsEnabled: true, + mapToolbarEnabled: false, + compassEnabled: false, + mapType: MapType.normal, + ), + ), + + // Coordinates + Container( + margin: const EdgeInsets.all(16).copyWith(left: 20, right: 20), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), + decoration: BoxDecoration( + color: const Color(0xFF1A1A2E), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withValues(alpha: 0.06)), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'LATITUDE', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF5A5A6E), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 4), + Text( + '${_lat.toStringAsFixed(4)}Β°', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFFF0F0F5), + ), + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'LONGITUDE', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF5A5A6E), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 4), + Text( + '${_lng.toStringAsFixed(4)}Β°', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFFF0F0F5), + ), + ), + ], + ), + ), + ], + ), + ), + + // Share Button + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _shareLocation, + icon: const Text('πŸ“€', style: TextStyle(fontSize: 18)), + label: const Text('Share Live Location'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF42A5F5), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + ), + ), + ), + ], + ), + ); + } +} + +class _ShareOption extends StatelessWidget { + final String icon; + final String label; + final Color color; + final VoidCallback onTap; + + const _ShareOption({ + required this.icon, + required this.label, + required this.color, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: const Color(0xFF1A1A2E), + borderRadius: BorderRadius.circular(14), + child: InkWell( + borderRadius: BorderRadius.circular(14), + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.white.withValues(alpha: 0.06)), + ), + child: Row( + children: [ + Text(icon, style: const TextStyle(fontSize: 22)), + const SizedBox(width: 14), + Expanded( + child: Text( + label, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFFF0F0F5), + ), + ), + ), + Icon(Icons.chevron_right, color: color, size: 20), + ], + ), + ), + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/login_screen.dart b/Team-Shivam/SheShield/lib/screens/login_screen.dart new file mode 100644 index 0000000..36af60e --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/login_screen.dart @@ -0,0 +1,315 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'home_screen.dart'; +import 'security_setup_screen.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _nameController = TextEditingController(); + bool _isLogin = true; // toggle between login & sign-up + bool _isLoading = false; + bool _obscurePassword = true; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + _nameController.dispose(); + super.dispose(); + } + + Future _submit() async { + final email = _emailController.text.trim(); + final password = _passwordController.text.trim(); + + if (email.isEmpty || password.isEmpty) { + _showPopup('⚠️ Missing Fields', 'Please enter both email and password.'); + return; + } + if (!_isLogin && _nameController.text.trim().isEmpty) { + _showPopup('⚠️ Missing Name', 'Please enter your name.'); + return; + } + + setState(() => _isLoading = true); + + try { + if (_isLogin) { + await FirebaseAuth.instance.signInWithEmailAndPassword( + email: email, + password: password, + ); + // Navigate to HomeScreen + if (mounted) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const HomeScreen()), + (route) => false, + ); + } + return; + } else { + final cred = await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: password, + ); + // Set display name + await cred.user?.updateDisplayName(_nameController.text.trim()); + // Navigate to Security Setup β†’ then Home + if (mounted) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const SecuritySetupScreen()), + (route) => false, + ); + } + return; + } + } on FirebaseAuthException catch (e) { + String msg; + switch (e.code) { + case 'user-not-found': + msg = 'No account found with this email.'; + break; + case 'wrong-password': + msg = 'Incorrect password. Please try again.'; + break; + case 'email-already-in-use': + msg = 'An account with this email already exists.'; + break; + case 'weak-password': + msg = 'Password is too weak. Use at least 6 characters.'; + break; + case 'invalid-email': + msg = 'Please enter a valid email address.'; + break; + default: + msg = e.message ?? 'Authentication failed.'; + } + if (mounted) _showPopup('❌ Error', msg); + } catch (e) { + if (mounted) _showPopup('❌ Error', 'Something went wrong. Please try again.'); + } + + if (mounted) setState(() => _isLoading = false); + } + + void _showPopup(String title, String message) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: const Color(0xFF1A1A2E), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text( + title, + style: const TextStyle( + color: Color(0xFFF0F0F5), + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + content: Text( + message, + style: const TextStyle(color: Color(0xFF8A8A9A), fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text( + 'OK', + style: TextStyle(color: Color(0xFFE53935), fontWeight: FontWeight.w700), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 60), + + // Logo + Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Image.asset( + 'assets/images/logo.png', + width: 72, + height: 72, + fit: BoxFit.contain, + ), + ), + ), + const SizedBox(height: 24), + + // Title + Center( + child: RichText( + text: const TextSpan( + style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, letterSpacing: -0.5), + children: [ + TextSpan(text: 'She', style: TextStyle(color: Color(0xFFF0F0F5))), + TextSpan(text: 'Shield', style: TextStyle(color: Color(0xFFE53935))), + ], + ), + ), + ), + const SizedBox(height: 6), + Center( + child: Text( + _isLogin ? 'Welcome back! Sign in to continue.' : 'Create your account to get started.', + style: const TextStyle(color: Color(0xFF8A8A9A), fontSize: 14), + ), + ), + + const SizedBox(height: 40), + + // Name field (sign-up only) + if (!_isLogin) ...[ + const Text( + 'FULL NAME', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF8A8A9A), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 6), + TextField( + controller: _nameController, + style: const TextStyle(color: Color(0xFFF0F0F5), fontSize: 15), + decoration: const InputDecoration( + hintText: 'e.g. Priya Sharma', + prefixIcon: Icon(Icons.person_outline, color: Color(0xFF5A5A6E), size: 20), + ), + textCapitalization: TextCapitalization.words, + ), + const SizedBox(height: 16), + ], + + // Email + const Text( + 'EMAIL', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF8A8A9A), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 6), + TextField( + controller: _emailController, + style: const TextStyle(color: Color(0xFFF0F0F5), fontSize: 15), + decoration: const InputDecoration( + hintText: 'you@example.com', + prefixIcon: Icon(Icons.email_outlined, color: Color(0xFF5A5A6E), size: 20), + ), + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 16), + + // Password + const Text( + 'PASSWORD', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF8A8A9A), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 6), + TextField( + controller: _passwordController, + style: const TextStyle(color: Color(0xFFF0F0F5), fontSize: 15), + obscureText: _obscurePassword, + decoration: InputDecoration( + hintText: 'β€’β€’β€’β€’β€’β€’β€’β€’', + prefixIcon: const Icon(Icons.lock_outline, color: Color(0xFF5A5A6E), size: 20), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: const Color(0xFF5A5A6E), + size: 20, + ), + onPressed: () => setState(() => _obscurePassword = !_obscurePassword), + ), + ), + ), + + const SizedBox(height: 28), + + // Submit button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading ? null : _submit, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + disabledBackgroundColor: const Color(0xFF2A2A3E), + ), + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : Text( + _isLogin ? 'Sign In' : 'Create Account', + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), + ), + ), + ), + + const SizedBox(height: 20), + + // Toggle login / sign-up + Center( + child: GestureDetector( + onTap: () => setState(() => _isLogin = !_isLogin), + child: RichText( + text: TextSpan( + style: const TextStyle(fontSize: 14), + children: [ + TextSpan( + text: _isLogin ? "Don't have an account? " : 'Already have an account? ', + style: const TextStyle(color: Color(0xFF8A8A9A)), + ), + TextSpan( + text: _isLogin ? 'Sign Up' : 'Sign In', + style: const TextStyle( + color: Color(0xFFE53935), + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/nearby_police_screen.dart b/Team-Shivam/SheShield/lib/screens/nearby_police_screen.dart new file mode 100644 index 0000000..d39c5f7 --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/nearby_police_screen.dart @@ -0,0 +1,407 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../services/location_service.dart'; +import '../services/places_service.dart'; + +class NearbyPoliceScreen extends StatefulWidget { + const NearbyPoliceScreen({super.key}); + + @override + State createState() => _NearbyPoliceScreenState(); +} + +class _NearbyPoliceScreenState extends State { + GoogleMapController? _mapController; + final Set _markers = {}; + + double _lat = 28.6139; + double _lng = 77.2090; + bool _loading = true; + String? _error; + List _stations = []; + + @override + void initState() { + super.initState(); + _init(); + } + + @override + void dispose() { + _mapController?.dispose(); + super.dispose(); + } + + Future _init() async { + try { + // 1. Get current location. + final pos = await LocationService.getCurrentLocation(); + _lat = pos.latitude; + _lng = pos.longitude; + + // 2. Move camera to actual user location. + _mapController?.animateCamera( + CameraUpdate.newLatLngZoom(LatLng(_lat, _lng), 13), + ); + + // 3. Fetch nearby stations. + await _fetchStations(); + } catch (e) { + if (mounted) { + setState(() { + _error = e.toString(); + _loading = false; + }); + } + } + } + + Future _fetchStations() async { + setState(() { + _loading = true; + _error = null; + }); + + try { + debugPrint('Fetching police stations near $_lat, $_lng ...'); + final stations = + await PlacesService.fetchNearbyPoliceStations(_lat, _lng); + debugPrint('Found ${stations.length} police stations'); + if (mounted) { + setState(() { + _stations = stations; + _loading = false; + _buildMarkers(); + }); + } + } catch (e) { + debugPrint('Error fetching police stations: $e'); + if (mounted) { + setState(() { + _error = 'Unable to load police stations. Check your internet connection and try again.'; + _loading = false; + }); + } + } + } + + void _buildMarkers() { + _markers.clear(); + + // User location marker. + _markers.add( + Marker( + markerId: const MarkerId('user_location'), + position: LatLng(_lat, _lng), + icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure), + infoWindow: const InfoWindow(title: 'Your Location'), + ), + ); + + // Police station markers. + for (final station in _stations) { + _markers.add( + Marker( + markerId: MarkerId(station.placeId), + position: LatLng(station.lat, station.lng), + icon: + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed), + infoWindow: InfoWindow( + title: station.name, + snippet: '${station.distanceText} Β· Tap to navigate', + onTap: () => _openNavigation(station), + ), + ), + ); + } + } + + Future _openNavigation(PoliceStation station) async { + // Try Google Maps navigation deep-link first. + final navUri = Uri.parse(PlacesService.getNavigationUrl( + station.lat, + station.lng, + )); + + if (await canLaunchUrl(navUri)) { + await launchUrl(navUri); + } else { + // Fallback to web URL. + final webUri = Uri.parse(PlacesService.getMapsUrl( + station.lat, + station.lng, + )); + await launchUrl(webUri, mode: LaunchMode.externalApplication); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Nearby Police Stations'), + leading: IconButton( + icon: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFF1A1A2E), + borderRadius: BorderRadius.circular(10), + border: + Border.all(color: Colors.white.withValues(alpha: 0.06)), + ), + child: const Icon(Icons.arrow_back, size: 18), + ), + onPressed: () => Navigator.pop(context), + ), + ), + body: Column( + children: [ + // Map + Expanded( + flex: 5, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withValues(alpha: 0.06)), + ), + clipBehavior: Clip.antiAlias, + child: Stack( + children: [ + GoogleMap( + initialCameraPosition: CameraPosition( + target: LatLng(_lat, _lng), + zoom: 13, + ), + markers: _markers, + onMapCreated: (controller) { + _mapController = controller; + // Re-center after map is ready if location already fetched + if (_lat != 28.6139 || _lng != 77.2090) { + controller.animateCamera( + CameraUpdate.newLatLngZoom(LatLng(_lat, _lng), 13), + ); + } + }, + myLocationEnabled: true, + myLocationButtonEnabled: false, + zoomControlsEnabled: true, + mapToolbarEnabled: false, + compassEnabled: false, + mapType: MapType.normal, + ), + if (_loading) + Container( + color: const Color(0xCC0A0A0F), + child: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + color: Color(0xFFE53935), + ), + SizedBox(height: 16), + Text( + 'Finding nearby police stations…', + style: TextStyle( + color: Color(0xFF8A8A9A), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + if (_error != null) + Container( + color: const Color(0xCC0A0A0F), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('⚠️', + style: TextStyle(fontSize: 36)), + const SizedBox(height: 12), + const Text( + 'Could not load stations', + style: TextStyle( + color: Color(0xFFF0F0F5), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _fetchStations, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE53935), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text('Retry'), + ), + ], + ), + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 12), + + // Station list + Expanded( + flex: 3, + child: _stations.isEmpty && !_loading + ? const Center( + child: Text( + 'No nearby police stations found', + style: TextStyle( + color: Color(0xFF5A5A6E), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ) + : ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: _stations.length, + separatorBuilder: (_, __) => + const SizedBox(height: 10), + itemBuilder: (_, i) => + _buildStationCard(_stations[i]), + ), + ), + + const SizedBox(height: 16), + ], + ), + ); + } + + Widget _buildStationCard(PoliceStation station) { + return Material( + color: const Color(0xFF1A1A2E), + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () { + // Animate camera to station. + _mapController?.animateCamera( + CameraUpdate.newLatLngZoom( + LatLng(station.lat, station.lng), + 15, + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withValues(alpha: 0.06)), + ), + child: Column( + children: [ + Row( + children: [ + // Icon + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: const Color(0x1F42A5F5), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Text('πŸš”', style: TextStyle(fontSize: 22)), + ), + ), + const SizedBox(width: 14), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + station.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFFF0F0F5), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + station.vicinity.isNotEmpty + ? station.vicinity + : 'Police Station', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF8A8A9A), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 8), + // Distance badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: const Color(0x2600E676), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + station.distanceText, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Color(0xFF00E676), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + // Get Directions button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => _openNavigation(station), + icon: const Icon(Icons.directions, size: 18), + label: const Text('Get Directions'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE53935), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + textStyle: const TextStyle( + fontFamily: 'Inter', + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/past_emergencies_screen.dart b/Team-Shivam/SheShield/lib/screens/past_emergencies_screen.dart new file mode 100644 index 0000000..45e5b7f --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/past_emergencies_screen.dart @@ -0,0 +1,545 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'video_player_screen.dart'; + +class PastEmergenciesScreen extends StatefulWidget { + const PastEmergenciesScreen({super.key}); + + @override + State createState() => _PastEmergenciesScreenState(); +} + +class _PastEmergenciesScreenState extends State { + List<_EmergencyItem> _items = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _loadEmergencies(); + } + + Future _loadEmergencies() async { + setState(() => _loading = true); + + final List<_EmergencyItem> items = []; + + // 1. Load alerts from Firestore + try { + final alertsSnap = await FirebaseFirestore.instance + .collection('alerts') + .orderBy('timestamp', descending: true) + .limit(50) + .get(); + + for (final doc in alertsSnap.docs) { + final data = doc.data(); + final ts = data['timestamp'] as Timestamp?; + items.add(_EmergencyItem( + id: doc.id, + type: 'alert', + title: data['notificationTitle'] ?? '🚨 Emergency Alert', + subtitle: data['mapsLink'] ?? 'Location shared', + status: data['status'] ?? 'unknown', + latitude: (data['latitude'] as num?)?.toDouble(), + longitude: (data['longitude'] as num?)?.toDouble(), + dateTime: ts?.toDate(), + videoUrl: null, + videoName: null, + )); + } + } catch (e) { + debugPrint('PastEmergencies: Failed to load alerts β€” $e'); + } + + // 2. Load video recordings from Firebase Storage + try { + final ref = FirebaseStorage.instance.ref('sos_recordings'); + final result = await ref.listAll(); + + for (final item in result.items) { + try { + final meta = await item.getMetadata(); + final url = await item.getDownloadURL(); + items.add(_EmergencyItem( + id: item.name, + type: 'video', + title: 'SOS Recording', + subtitle: _formatSize(meta.size ?? 0), + status: 'recorded', + dateTime: meta.timeCreated, + videoUrl: url, + videoName: item.name, + )); + } catch (_) {} + } + } catch (e) { + debugPrint('PastEmergencies: Failed to load videos β€” $e'); + } + + // 3. Load local emergency records from SharedPreferences + try { + final prefs = await SharedPreferences.getInstance(); + final localList = prefs.getStringList('emergencies') ?? []; + for (final jsonStr in localList) { + try { + final data = jsonDecode(jsonStr) as Map; + final time = DateTime.tryParse(data['time'] ?? ''); + final filePath = data['filePath'] as String? ?? ''; + final type = data['type'] as String? ?? 'video'; + final fileExists = filePath.isNotEmpty && await File(filePath).exists(); + + items.add(_EmergencyItem( + id: 'local_${time?.millisecondsSinceEpoch ?? items.length}', + type: type, + title: 'SOS Recording (Local)', + subtitle: fileExists ? 'Saved locally' : 'File not found', + status: fileExists ? 'saved' : 'missing', + dateTime: time, + videoUrl: null, + videoName: null, + localFilePath: fileExists ? filePath : null, + )); + } catch (_) {} + } + } catch (e) { + debugPrint('PastEmergencies: Failed to load local records β€” \$e'); + } + + // Sort by newest first + items.sort((a, b) { + if (a.dateTime == null && b.dateTime == null) return 0; + if (a.dateTime == null) return 1; + if (b.dateTime == null) return -1; + return b.dateTime!.compareTo(a.dateTime!); + }); + + if (mounted) { + setState(() { + _items = items; + _loading = false; + }); + } + } + + void _openMap(double lat, double lng) async { + final url = 'https://maps.google.com/?q=$lat,$lng'; + try { + await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + } catch (_) { + await Clipboard.setData(ClipboardData(text: url)); + if (mounted) _showPopup('πŸ“‹ Link Copied', 'Map link copied to clipboard.'); + } + } + + void _openVideo(String url) async { + try { + await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + } catch (_) { + await Clipboard.setData(ClipboardData(text: url)); + if (mounted) _showPopup('πŸ“‹ Link Copied', 'Video link copied to clipboard.'); + } + } + + void _playLocalFile(String filePath) async { + final file = File(filePath); + if (await file.exists()) { + if (mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => VideoPlayerScreen( + filePath: filePath, + title: 'SOS Recording', + ), + ), + ); + } + } else { + if (mounted) _showPopup('⚠️ Error', 'Recording file not found.'); + } + } + + Future _deleteItem(_EmergencyItem item) async { + final confirm = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: const Text('πŸ—‘οΈ Delete Record?', + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 18)), + content: Text( + item.type == 'video' + ? 'This recording will be permanently deleted.' + : 'This alert record will be permanently deleted.', + style: const TextStyle(fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text('Cancel', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5))), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Delete', + style: TextStyle( + color: Color(0xFFE53935), fontWeight: FontWeight.w700)), + ), + ], + ), + ); + + if (confirm != true) return; + + try { + if (item.type == 'video' && item.videoName != null) { + await FirebaseStorage.instance + .ref('sos_recordings/${item.videoName}') + .delete(); + } else if (item.type == 'alert') { + await FirebaseFirestore.instance + .collection('alerts') + .doc(item.id) + .delete(); + } + setState(() => _items.remove(item)); + if (mounted) _showPopup('βœ… Deleted', 'Record has been removed.'); + } catch (_) { + if (mounted) _showPopup('⚠️ Error', 'Could not delete record.'); + } + } + + void _showPopup(String title, String message) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text(title, + style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 18)), + content: Text(message, style: const TextStyle(fontSize: 14)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('OK', + style: TextStyle( + color: Color(0xFFE53935), fontWeight: FontWeight.w700)), + ), + ], + ), + ); + } + + String _formatSize(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + } + + String _formatDate(DateTime? dt) { + if (dt == null) return 'Unknown date'; + final d = dt.toLocal(); + final months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + final hour = d.hour > 12 ? d.hour - 12 : (d.hour == 0 ? 12 : d.hour); + final amPm = d.hour >= 12 ? 'PM' : 'AM'; + return '${d.day} ${months[d.month - 1]} ${d.year}, $hour:${d.minute.toString().padLeft(2, '0')} $amPm'; + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Scaffold( + appBar: AppBar( + title: const Text('Past Emergencies'), + leading: IconButton( + icon: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1A1A2E) : Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08)), + ), + child: const Icon(Icons.arrow_back, size: 18), + ), + onPressed: () => Navigator.pop(context), + ), + ), + body: _loading + ? const Center( + child: CircularProgressIndicator(color: Color(0xFFE53935))) + : RefreshIndicator( + onRefresh: _loadEmergencies, + color: const Color(0xFFE53935), + child: _items.isEmpty + ? ListView( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('πŸ“‹', + style: TextStyle(fontSize: 48)), + const SizedBox(height: 12), + Text( + 'No past emergencies', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + ), + ), + const SizedBox(height: 4), + Text( + 'SOS alerts and recordings will appear here', + style: TextStyle( + fontSize: 12, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + ), + ), + ], + ), + ), + ), + ], + ) + : ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + itemCount: _items.length, + separatorBuilder: (_, __) => + const SizedBox(height: 10), + itemBuilder: (_, i) => + _buildEmergencyCard(_items[i], isDark), + ), + ), + ); + } + + Widget _buildEmergencyCard(_EmergencyItem item, bool isDark) { + final isAlert = item.type == 'alert'; + final isActive = item.status == 'active'; + final bg = isDark ? const Color(0xFF1A1A2E) : Colors.white; + final border = isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08); + + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isActive + ? const Color(0xFFE53935).withValues(alpha: 0.3) + : border, + ), + ), + child: Row( + children: [ + // Icon + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: isAlert + ? const Color(0xFFE53935).withValues(alpha: 0.12) + : const Color(0xFF42A5F5).withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Icon( + isAlert ? Icons.warning_amber_rounded : Icons.videocam_rounded, + color: isAlert + ? const Color(0xFFE53935) + : const Color(0xFF42A5F5), + size: 24, + ), + ), + ), + const SizedBox(width: 14), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + isAlert ? 'Emergency Alert' : 'SOS Recording', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark + ? const Color(0xFFF0F0F5) + : const Color(0xFF1A1A2E), + ), + ), + ), + if (isActive) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: + const Color(0xFFE53935).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + ), + child: const Text('ACTIVE', + style: TextStyle( + fontSize: 8, + fontWeight: FontWeight.w800, + color: Color(0xFFE53935), + letterSpacing: 0.5, + )), + ), + ], + ], + ), + const SizedBox(height: 2), + Text( + _formatDate(item.dateTime), + style: TextStyle( + fontSize: 12, + color: isDark + ? const Color(0xFF8A8A9A) + : const Color(0xFF5A5A6E), + ), + ), + if (!isAlert) + Text( + item.subtitle, + style: TextStyle( + fontSize: 11, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + ), + ), + ], + ), + ), + // Action buttons + if (isAlert && item.latitude != null && item.longitude != null) + GestureDetector( + onTap: () => _openMap(item.latitude!, item.longitude!), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFF42A5F5).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Icon(Icons.map_outlined, + color: Color(0xFF42A5F5), size: 18), + ), + ), + ), + if (!isAlert && item.videoUrl != null) + GestureDetector( + onTap: () => _openVideo(item.videoUrl!), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFF42A5F5).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Icon(Icons.play_arrow_rounded, + color: Color(0xFF42A5F5), size: 20), + ), + ), + ), + // Local file play button + if (!isAlert && item.localFilePath != null) + GestureDetector( + onTap: () => _playLocalFile(item.localFilePath!), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFF00E676).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Icon(Icons.play_circle_outline_rounded, + color: Color(0xFF00E676), size: 20), + ), + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () => _deleteItem(item), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + border: Border.all(color: border), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Icon(Icons.delete_outline, + color: isDark + ? const Color(0xFF5A5A6E) + : const Color(0xFF8A8A9A), + size: 18), + ), + ), + ), + ], + ), + ); + } +} + +class _EmergencyItem { + final String id; + final String type; // 'alert' or 'video' + final String title; + final String subtitle; + final String status; + final double? latitude; + final double? longitude; + final DateTime? dateTime; + final String? videoUrl; + final String? videoName; + final String? localFilePath; + + _EmergencyItem({ + required this.id, + required this.type, + required this.title, + required this.subtitle, + required this.status, + this.latitude, + this.longitude, + required this.dateTime, + this.videoUrl, + this.videoName, + this.localFilePath, + }); +} diff --git a/Team-Shivam/SheShield/lib/screens/profile_screen.dart b/Team-Shivam/SheShield/lib/screens/profile_screen.dart new file mode 100644 index 0000000..6fb4b4d --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/profile_screen.dart @@ -0,0 +1,438 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../main.dart'; +import 'login_screen.dart'; + +class ProfileScreen extends StatefulWidget { + const ProfileScreen({super.key}); + + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + bool _isDark = themeNotifier.value == ThemeMode.dark; + + void _toggleTheme(bool dark) async { + setState(() => _isDark = dark); + themeNotifier.value = dark ? ThemeMode.dark : ThemeMode.light; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('dark_mode', dark); + } + + void _confirmLogout() { + final theme = Theme.of(context); + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: theme.colorScheme.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text( + 'πŸ‘‹ Sign Out?', + style: TextStyle(color: theme.colorScheme.onSurface, fontWeight: FontWeight.w700, fontSize: 18), + ), + content: Text( + 'Are you sure you want to sign out of SheShield?', + style: TextStyle(color: theme.colorScheme.onSurface.withValues(alpha: 0.6), fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('Cancel', style: TextStyle(color: theme.colorScheme.onSurface.withValues(alpha: 0.5))), + ), + TextButton( + onPressed: () async { + Navigator.pop(ctx); // close dialog + await FirebaseAuth.instance.signOut(); + if (context.mounted) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const LoginScreen()), + (route) => false, // remove all routes + ); + } + }, + child: const Text('Sign Out', style: TextStyle(color: Color(0xFFE53935), fontWeight: FontWeight.w700)), + ), + ], + ), + ); + } + + String _formatDate(DateTime dt) { + final d = dt.toLocal(); + final months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + return '${d.day} ${months[d.month - 1]} ${d.year}'; + } + + @override + Widget build(BuildContext context) { + final user = FirebaseAuth.instance.currentUser; + final name = user?.displayName ?? 'User'; + final email = user?.email ?? ''; + final initial = name.isNotEmpty ? name[0].toUpperCase() : '?'; + final theme = Theme.of(context); + final isDarkMode = theme.brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: const Text('Profile'), + leading: IconButton( + icon: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: theme.colorScheme.onSurface.withValues(alpha: 0.06)), + ), + child: const Icon(Icons.arrow_back, size: 18), + ), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + const SizedBox(height: 40), + + // Avatar + Container( + width: 88, + height: 88, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFE53935), Color(0xFFC62828)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: const Color(0xFFE53935).withValues(alpha: 0.2), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + child: Center( + child: Text( + initial, + style: const TextStyle(fontSize: 36, fontWeight: FontWeight.w800, color: Colors.white), + ), + ), + ), + const SizedBox(height: 16), + + // Name + Text( + name, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: theme.colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + email, + style: TextStyle(fontSize: 14, color: theme.colorScheme.onSurface.withValues(alpha: 0.5)), + ), + + const SizedBox(height: 32), + + // ─── Theme Toggle ─── + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: theme.colorScheme.onSurface.withValues(alpha: 0.06)), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFE53935).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Icon( + isDarkMode ? Icons.dark_mode : Icons.light_mode, + color: const Color(0xFFE53935), + size: 20, + ), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'APPEARANCE', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + const SizedBox(height: 2), + Text( + _isDark ? 'Dark Mode' : 'Light Mode', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + ], + ), + ), + Switch( + value: _isDark, + onChanged: _toggleTheme, + activeThumbColor: const Color(0xFFE53935), + activeTrackColor: const Color(0xFFE53935).withValues(alpha: 0.3), + ), + ], + ), + ), + + const SizedBox(height: 10), + + // Info cards + _InfoTile(icon: Icons.email_outlined, label: 'Email', value: email), + const SizedBox(height: 10), + _EditableNameTile( + name: name, + onNameChanged: () => setState(() {}), + ), + const SizedBox(height: 10), + _InfoTile( + icon: Icons.calendar_today_outlined, + label: 'Joined', + value: user?.metadata.creationTime != null + ? _formatDate(user!.metadata.creationTime!) + : 'Unknown', + ), + + const SizedBox(height: 32), + + // Logout button + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _confirmLogout, + icon: const Icon(Icons.logout_rounded, size: 20), + label: const Text('Sign Out'), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFE53935), + side: const BorderSide(color: Color(0xFFE53935), width: 1.5), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), + ), + ), + ), + + const SizedBox(height: 40), + ], + ), + ), + ); + } +} + +class _InfoTile extends StatelessWidget { + final IconData icon; + final String label; + final String value; + + const _InfoTile({ + required this.icon, + required this.label, + required this.value, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: theme.colorScheme.onSurface.withValues(alpha: 0.06)), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFE53935).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Center(child: Icon(icon, color: const Color(0xFFE53935), size: 20)), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _EditableNameTile extends StatelessWidget { + final String name; + final VoidCallback onNameChanged; + + const _EditableNameTile({required this.name, required this.onNameChanged}); + + void _showEditDialog(BuildContext context) { + final controller = TextEditingController(text: name == 'User' ? '' : name); + final theme = Theme.of(context); + + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: theme.colorScheme.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text('✏️ Edit Name', + style: TextStyle( + color: theme.colorScheme.onSurface, + fontWeight: FontWeight.w700, + fontSize: 18)), + content: TextField( + controller: controller, + autofocus: true, + textCapitalization: TextCapitalization.words, + style: TextStyle(color: theme.colorScheme.onSurface, fontSize: 15), + decoration: const InputDecoration( + hintText: 'Enter your name', + prefixIcon: Icon(Icons.person_outline, size: 20), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('Cancel', + style: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.5))), + ), + TextButton( + onPressed: () async { + final newName = controller.text.trim(); + if (newName.isEmpty) return; + Navigator.pop(ctx); + try { + await FirebaseAuth.instance.currentUser + ?.updateDisplayName(newName); + await FirebaseAuth.instance.currentUser?.reload(); + onNameChanged(); + } catch (_) {} + }, + child: const Text('Save', + style: TextStyle( + color: Color(0xFFE53935), fontWeight: FontWeight.w700)), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: theme.colorScheme.onSurface.withValues(alpha: 0.06)), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFE53935).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: + Icon(Icons.badge_outlined, color: Color(0xFFE53935), size: 20)), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Display Name', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: + theme.colorScheme.onSurface.withValues(alpha: 0.5), + )), + const SizedBox(height: 2), + Text(name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + overflow: TextOverflow.ellipsis), + ], + ), + ), + GestureDetector( + onTap: () => _showEditDialog(context), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFFE53935).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Icon(Icons.edit_outlined, + color: Color(0xFFE53935), size: 16), + ), + ), + ), + ], + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/security_setup_screen.dart b/Team-Shivam/SheShield/lib/screens/security_setup_screen.dart new file mode 100644 index 0000000..8c545a3 --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/security_setup_screen.dart @@ -0,0 +1,327 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'home_screen.dart'; + +/// Blocking screen that requires the user to confirm they've enabled +/// power-off verification in device settings before accessing the app. +/// +/// Uses SharedPreferences to persist the confirmation so it only shows once. +class SecuritySetupScreen extends StatefulWidget { + const SecuritySetupScreen({super.key}); + + /// Check if security setup has been completed previously. + static Future isCompleted() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool('security_setup_done') ?? false; + } + + @override + State createState() => _SecuritySetupScreenState(); +} + +class _SecuritySetupScreenState extends State { + bool _confirmed = false; + + /// Open Android security settings. + Future _openSecuritySettings() async { + // Try opening a helpful guide for the user + try { + // Use the Android Settings activity action + await launchUrl( + Uri.parse('https://www.google.com/search?q=how+to+enable+power+off+verification'), + mode: LaunchMode.externalApplication, + ); + } catch (_) {} + + // Also try opening the actual settings page + try { + await launchUrl( + Uri.parse('content://settings/system'), + mode: LaunchMode.externalApplication, + ); + } catch (_) { + // Fallback: show instruction + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Go to Settings β†’ Security β†’ Power off verification'), + duration: Duration(seconds: 4), + ), + ); + } + } + } + + /// Mark setup as done and proceed to home. + Future _completeSetup() async { + if (!_confirmed) return; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('security_setup_done', true); + if (mounted) { + // If this is the root screen (after signup), go to HomeScreen + // Otherwise pop back to SplashGate which will show HomeScreen + final canPop = Navigator.of(context).canPop(); + if (canPop) { + Navigator.of(context).pop(true); + } else { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const HomeScreen()), + (route) => false, + ); + } + } + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final cardBg = isDark ? const Color(0xFF1A1A2E) : Colors.white; + final border = isDark + ? Colors.white.withValues(alpha: 0.06) + : Colors.black.withValues(alpha: 0.08); + + return PopScope( + canPop: false, // Block back button + child: Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: isDark + ? [const Color(0xFF0A0A0F), const Color(0xFF1A1A2E)] + : [const Color(0xFFF5F5F8), Colors.white], + ), + ), + child: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Warning icon ── + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: const Color(0xFFFF9800).withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + child: const Center( + child: Text('⚠️', style: TextStyle(fontSize: 40)), + ), + ), + const SizedBox(height: 24), + // ── Title ── + Text( + 'Security Setup Required', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w800, + color: isDark + ? const Color(0xFFF0F0F5) + : const Color(0xFF1A1A2E), + letterSpacing: -0.3, + ), + ), + const SizedBox(height: 20), + // ── Info card ── + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: border), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFF9800).withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'For your safety, please enable power-off verification in your device settings. This prevents the phone from being switched off during emergencies.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + height: 1.5, + color: isDark + ? const Color(0xFFC0C0D0) + : const Color(0xFF3A3A4E), + ), + ), + ), + const SizedBox(height: 16), + // ── Steps ── + _buildStep(isDark, '1', 'Open device Settings'), + const SizedBox(height: 8), + _buildStep(isDark, '2', 'Go to Security / Lock Screen'), + const SizedBox(height: 8), + _buildStep(isDark, '3', 'Enable "Power off verification"'), + ], + ), + ), + const SizedBox(height: 20), + // ── Go to Settings button ── + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _openSecuritySettings, + icon: const Icon(Icons.settings, size: 20), + label: const Text( + 'Go to Settings', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF9800), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + elevation: 0, + ), + ), + ), + const SizedBox(height: 16), + // ── Confirmation checkbox ── + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isDark + ? Colors.white.withValues(alpha: 0.04) + : Colors.black.withValues(alpha: 0.03), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _confirmed + ? const Color(0xFF00E676).withValues(alpha: 0.3) + : border, + ), + ), + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: Checkbox( + value: _confirmed, + onChanged: (val) => + setState(() => _confirmed = val ?? false), + activeColor: const Color(0xFF00E676), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'I confirm I have enabled this setting', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isDark + ? const Color(0xFFC0C0D0) + : const Color(0xFF3A3A4E), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + // ── I Have Enabled It button ── + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _confirmed ? _completeSetup : null, + icon: Icon( + _confirmed + ? Icons.check_circle + : Icons.lock_outline, + size: 20, + ), + label: const Text( + 'I Have Enabled It', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: _confirmed + ? const Color(0xFF00E676) + : const Color(0xFF5A5A6E), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + elevation: 0, + disabledBackgroundColor: + isDark ? const Color(0xFF2A2A3E) : const Color(0xFFE0E0E0), + disabledForegroundColor: + isDark ? const Color(0xFF5A5A6E) : const Color(0xFF9A9A9A), + ), + ), + ), + const SizedBox(height: 32), + ], + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildStep(bool isDark, String num, String text) { + return Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: const Color(0xFFFF9800).withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + num, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w800, + color: Color(0xFFFF9800), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark + ? const Color(0xFFC0C0D0) + : const Color(0xFF3A3A4E), + ), + ), + ), + ], + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/sos_screen.dart b/Team-Shivam/SheShield/lib/screens/sos_screen.dart new file mode 100644 index 0000000..a8a946d --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/sos_screen.dart @@ -0,0 +1,253 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Full-screen emergency SOS alert. +/// +/// Shows a flashing red overlay, large alert text, continuous +/// vibration pattern, and a cancel button. Push this screen +/// when "BUTTON SOS" or "MOTION SOS" is received. +class SOSScreen extends StatefulWidget { + final VoidCallback onCancel; + const SOSScreen({super.key, required this.onCancel}); + + @override + State createState() => _SOSScreenState(); +} + +class _SOSScreenState extends State + with TickerProviderStateMixin { + late AnimationController _flashController; + late AnimationController _pulseController; + late AnimationController _textFadeController; + Timer? _vibrationTimer; + int _dotCount = 0; + Timer? _dotTimer; + + @override + void initState() { + super.initState(); + + // Flash animation β€” alternates red shades + _flashController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + )..repeat(reverse: true); + + // Pulse scale for the icon + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(reverse: true); + + // Fade-in for text + _textFadeController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + )..forward(); + + // Continuous vibration + _startVibration(); + + // Animated dots for "Sending help signal..." + _dotTimer = Timer.periodic(const Duration(milliseconds: 500), (_) { + if (mounted) setState(() => _dotCount = (_dotCount + 1) % 4); + }); + } + + void _startVibration() { + HapticFeedback.heavyImpact(); + _vibrationTimer = Timer.periodic(const Duration(milliseconds: 800), (_) { + HapticFeedback.heavyImpact(); + }); + } + + @override + void dispose() { + _flashController.dispose(); + _pulseController.dispose(); + _textFadeController.dispose(); + _vibrationTimer?.cancel(); + _dotTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, // prevent back button + child: AnimatedBuilder( + animation: _flashController, + builder: (context, child) { + final flashValue = _flashController.value; + return Scaffold( + backgroundColor: Color.lerp( + const Color(0xFFB71C1C), + const Color(0xFFE53935), + flashValue, + ), + body: SafeArea( + child: Column( + children: [ + const Spacer(flex: 2), + // ── Pulsing alert icon ── + AnimatedBuilder( + animation: _pulseController, + builder: (context, _) { + final scale = + 1.0 + (_pulseController.value * 0.15); + return Transform.scale( + scale: scale, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.15), + boxShadow: [ + BoxShadow( + color: + Colors.white.withValues(alpha: 0.1 * flashValue), + blurRadius: 60, + spreadRadius: 20, + ), + ], + ), + child: const Center( + child: Text( + '🚨', + style: TextStyle(fontSize: 56), + ), + ), + ), + ); + }, + ), + const SizedBox(height: 40), + // ── Title ── + FadeTransition( + opacity: _textFadeController, + child: const Text( + 'EMERGENCY\nTRIGGERED', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w900, + color: Colors.white, + letterSpacing: 2, + height: 1.2, + ), + ), + ), + const SizedBox(height: 16), + // ── Subtitle with animated dots ── + FadeTransition( + opacity: _textFadeController, + child: Text( + 'Sending help signal${'.' * _dotCount}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white.withValues(alpha: 0.8), + letterSpacing: 0.5, + ), + ), + ), + const SizedBox(height: 8), + FadeTransition( + opacity: _textFadeController, + child: Text( + 'GPS location shared β€’ Contacts alerted', + style: TextStyle( + fontSize: 13, + color: Colors.white.withValues(alpha: 0.6), + ), + ), + ), + const Spacer(flex: 2), + // ── Info chips ── + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _infoChip(Icons.location_on, 'Location\nSent'), + _infoChip(Icons.message, 'SMS\nSent'), + _infoChip(Icons.videocam, 'Recording\nActive'), + ], + ), + ), + const Spacer(), + // ── Cancel button ── + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: widget.onCancel, + icon: const Icon(Icons.close, size: 20), + label: const Text('Cancel Alert'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: BorderSide( + color: Colors.white.withValues(alpha: 0.4), + width: 2), + padding: const EdgeInsets.symmetric(vertical: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Hold to cancel', + style: TextStyle( + fontSize: 12, + color: Colors.white.withValues(alpha: 0.4), + ), + ), + const SizedBox(height: 40), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _infoChip(IconData icon, String label) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(14), + border: + Border.all(color: Colors.white.withValues(alpha: 0.15)), + ), + child: Column( + children: [ + Icon(icon, color: Colors.white, size: 22), + const SizedBox(height: 6), + Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.white.withValues(alpha: 0.8), + height: 1.3, + ), + ), + ], + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/screens/video_player_screen.dart b/Team-Shivam/SheShield/lib/screens/video_player_screen.dart new file mode 100644 index 0000000..99ca2a6 --- /dev/null +++ b/Team-Shivam/SheShield/lib/screens/video_player_screen.dart @@ -0,0 +1,221 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +/// Simple full-screen video player for local SOS recordings. +class VideoPlayerScreen extends StatefulWidget { + final String filePath; + final String title; + + const VideoPlayerScreen({ + super.key, + required this.filePath, + this.title = 'SOS Recording', + }); + + @override + State createState() => _VideoPlayerScreenState(); +} + +class _VideoPlayerScreenState extends State { + late VideoPlayerController _controller; + bool _initialized = false; + String? _error; + + @override + void initState() { + super.initState(); + _initPlayer(); + } + + Future _initPlayer() async { + try { + final file = File(widget.filePath); + if (!await file.exists()) { + setState(() => _error = 'Recording file not found'); + return; + } + + _controller = VideoPlayerController.file(file); + await _controller.initialize(); + await _controller.play(); + if (mounted) setState(() => _initialized = true); + } catch (e) { + if (mounted) setState(() => _error = 'Could not play video: $e'); + } + } + + @override + void dispose() { + if (_initialized) _controller.dispose(); + super.dispose(); + } + + String _formatDuration(Duration d) { + final mins = d.inMinutes.remainder(60).toString().padLeft(2, '0'); + final secs = d.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$mins:$secs'; + } + + @override + Widget build(BuildContext context) { + + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + title: Text(widget.title, + style: const TextStyle(color: Colors.white, fontSize: 16)), + iconTheme: const IconThemeData(color: Colors.white), + ), + body: _error != null + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 48), + const SizedBox(height: 12), + Text(_error!, + style: const TextStyle(color: Colors.white70, fontSize: 14)), + ], + ), + ) + : !_initialized + ? const Center( + child: CircularProgressIndicator(color: Color(0xFFE53935))) + : Column( + children: [ + Expanded( + child: Center( + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ), + ), + ), + // Controls + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 16), + color: Colors.black, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Progress bar + ValueListenableBuilder( + valueListenable: _controller, + builder: (_, value, __) { + return Column( + children: [ + SliderTheme( + data: SliderThemeData( + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6), + overlayShape: + const RoundSliderOverlayShape( + overlayRadius: 14), + activeTrackColor: + const Color(0xFFE53935), + inactiveTrackColor: Colors.white24, + thumbColor: const Color(0xFFE53935), + ), + child: Slider( + value: value.position.inMilliseconds + .toDouble(), + max: value.duration.inMilliseconds + .toDouble() + .clamp(1, double.infinity), + onChanged: (v) { + _controller.seekTo(Duration( + milliseconds: v.toInt())); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(value.position), + style: const TextStyle( + color: Colors.white60, + fontSize: 12), + ), + Text( + _formatDuration(value.duration), + style: const TextStyle( + color: Colors.white60, + fontSize: 12), + ), + ], + ), + ), + ], + ); + }, + ), + const SizedBox(height: 8), + // Play/Pause button + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + _controller.seekTo( + _controller.value.position - + const Duration(seconds: 5)); + }, + icon: const Icon(Icons.replay_5, + color: Colors.white, size: 32), + ), + const SizedBox(width: 16), + ValueListenableBuilder( + valueListenable: _controller, + builder: (_, value, __) { + return GestureDetector( + onTap: () { + value.isPlaying + ? _controller.pause() + : _controller.play(); + }, + child: Container( + width: 56, + height: 56, + decoration: const BoxDecoration( + color: Color(0xFFE53935), + shape: BoxShape.circle, + ), + child: Icon( + value.isPlaying + ? Icons.pause + : Icons.play_arrow, + color: Colors.white, + size: 32, + ), + ), + ); + }, + ), + const SizedBox(width: 16), + IconButton( + onPressed: () { + _controller.seekTo( + _controller.value.position + + const Duration(seconds: 5)); + }, + icon: const Icon(Icons.forward_5, + color: Colors.white, size: 32), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/Team-Shivam/SheShield/lib/services/alert_service.dart b/Team-Shivam/SheShield/lib/services/alert_service.dart new file mode 100644 index 0000000..c944f39 --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/alert_service.dart @@ -0,0 +1,63 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'storage_service.dart'; + +/// Service for broadcasting emergency alerts to Firestore and +/// preparing FCM notification data for emergency contacts. +class AlertService { + static final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + /// Broadcast an emergency alert. + /// + /// 1. Generates a Google Maps link from [lat]/[lng]. + /// 2. Creates a Firestore document in the `alerts` collection. + /// 3. Reads local emergency contacts and stores their info alongside the + /// alert so a Cloud Function can deliver FCM push notifications. + /// + /// Returns the Firestore document ID of the created alert, or `null` on + /// failure. + static Future broadcastAlert({ + required double lat, + required double lng, + String? userId, + }) async { + try { + // 1. Generate Google Maps link. + final mapsLink = 'https://maps.google.com/?q=$lat,$lng'; + + // 2. Load emergency contacts from local storage. + final contacts = await StorageService.loadContacts(); + final contactList = contacts + .map((c) => {'name': c.name, 'phone': c.phone}) + .toList(); + + // 3. Create the alert document. + final docRef = await _firestore.collection('alerts').add({ + 'userId': userId ?? 'anonymous', + 'latitude': lat, + 'longitude': lng, + 'mapsLink': mapsLink, + 'status': 'active', + 'contacts': contactList, + 'notificationTitle': '🚨 Emergency Alert', + 'notificationBody': + 'A user needs help.\nLive location: $mapsLink', + 'timestamp': FieldValue.serverTimestamp(), + }); + + return docRef.id; + } catch (_) { + return null; + } + } + + /// Mark an alert as resolved / cancelled. + static Future cancelAlert(String alertId) async { + try { + await _firestore.collection('alerts').doc(alertId).update({ + 'status': 'cancelled', + }); + } catch (_) { + // Silently fail. + } + } +} diff --git a/Team-Shivam/SheShield/lib/services/bluetooth_service.dart b/Team-Shivam/SheShield/lib/services/bluetooth_service.dart new file mode 100644 index 0000000..ac4fda5 --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/bluetooth_service.dart @@ -0,0 +1,285 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bluetooth_serial_ble/flutter_bluetooth_serial_ble.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Connection states exposed to the UI. +enum BtConnectionState { disconnected, connecting, connected } + +/// Commands recognized from ESP32 serial data. +enum BraceletCommand { sos, shake, voice, camera } + +typedef BraceletCommandCallback = void Function(BraceletCommand command); + +/// **Global singleton** Bluetooth Classic service. +/// +/// Features: +/// - Persistent connection across all screen navigations +/// - Auto-reconnect on connection drop (3 retries, 5 s interval) +/// - Saves last-connected device for auto-connect on app launch +/// - Exposes reactive [connectionNotifier] for UI binding +/// - Parses "BUTTON SOS", "MOTION SOS", "SHAKE" from serial stream +class BluetoothService extends ChangeNotifier { + // ── Singleton ────────────────────────────────────────────── + BluetoothService._(); + static final BluetoothService instance = BluetoothService._(); + + // ── Navigator key for global SOS push ────────────────────── + static final GlobalKey navigatorKey = + GlobalKey(); + + // ── Internal state ───────────────────────────────────────── + BluetoothConnection? _connection; + StreamSubscription? _inputSub; + String _buffer = ''; + + BtConnectionState _state = BtConnectionState.disconnected; + String _deviceName = ''; + String _deviceAddress = ''; + + Timer? _reconnectTimer; + int _reconnectAttempts = 0; + static const int _maxReconnectAttempts = 5; + static const Duration _reconnectInterval = Duration(seconds: 5); + + bool _userDisconnected = false; // true when user explicitly disconnects + + // ── Public getters ───────────────────────────────────────── + BtConnectionState get state => _state; + bool get isConnected => _state == BtConnectionState.connected; + bool get isConnecting => _state == BtConnectionState.connecting; + String get deviceName => _deviceName; + String get deviceAddress => _deviceAddress; + + /// Global callback β€” fires from ANY screen when a command is received. + BraceletCommandCallback? onCommand; + + // ── Persistence keys ─────────────────────────────────────── + static const _kDeviceName = 'bt_last_device_name'; + static const _kDeviceAddress = 'bt_last_device_address'; + + // ══════════════════════════════════════════════════════════ + // AUTO-CONNECT ON APP LAUNCH + // ══════════════════════════════════════════════════════════ + + /// Call once from main(). Tries to reconnect to the last saved device. + Future tryAutoConnect() async { + final prefs = await SharedPreferences.getInstance(); + final address = prefs.getString(_kDeviceAddress); + final name = prefs.getString(_kDeviceName); + if (address == null || address.isEmpty) return; + + _deviceName = name ?? address; + _deviceAddress = address; + + debugPrint('BluetoothService: Auto-connecting to $_deviceName…'); + await _connectToAddress(address); + } + + // ══════════════════════════════════════════════════════════ + // CONNECT / DISCONNECT + // ══════════════════════════════════════════════════════════ + + /// Connect to a device via RFCOMM and start listening. + Future connect(BluetoothDevice device) async { + // Already connected to this device + if (isConnected && _deviceAddress == device.address) return true; + + // Disconnect previous if any + if (isConnected) disconnect(); + + _userDisconnected = false; + _deviceName = device.name ?? device.address; + _deviceAddress = device.address; + + // Persist for auto-connect + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_kDeviceName, _deviceName); + await prefs.setString(_kDeviceAddress, _deviceAddress); + + return _connectToAddress(device.address); + } + + Future _connectToAddress(String address) async { + _setState(BtConnectionState.connecting); + + try { + debugPrint('BluetoothService: Connecting to $address…'); + _connection = await BluetoothConnection.toAddress(address) + .timeout(const Duration(seconds: 10)); + debugPrint('BluetoothService: βœ… Connected to $_deviceName'); + + _inputSub?.cancel(); + _inputSub = _connection!.input?.listen( + _handleData, + onDone: () { + debugPrint('BluetoothService: Connection closed by remote'); + _onUnexpectedDisconnect(); + }, + onError: (e) { + debugPrint('BluetoothService: Stream error β€” $e'); + _onUnexpectedDisconnect(); + }, + ); + + _reconnectAttempts = 0; + _reconnectTimer?.cancel(); + _setState(BtConnectionState.connected); + return true; + } catch (e) { + debugPrint('BluetoothService: Connection failed β€” $e'); + _setState(BtConnectionState.disconnected); + return false; + } + } + + /// Explicitly disconnect (user action). + void disconnect() { + _userDisconnected = true; + _reconnectTimer?.cancel(); + _reconnectAttempts = 0; + _cleanupConnection(); + _setState(BtConnectionState.disconnected); + debugPrint('BluetoothService: User disconnected'); + } + + void _cleanupConnection() { + _inputSub?.cancel(); + _inputSub = null; + try { + _connection?.finish(); + } catch (_) {} + _connection = null; + _buffer = ''; + } + + // ══════════════════════════════════════════════════════════ + // AUTO-RECONNECT + // ══════════════════════════════════════════════════════════ + + void _onUnexpectedDisconnect() { + _cleanupConnection(); + _setState(BtConnectionState.disconnected); + + if (_userDisconnected || _deviceAddress.isEmpty) return; + + debugPrint('BluetoothService: Starting auto-reconnect…'); + _startReconnect(); + } + + void _startReconnect() { + _reconnectTimer?.cancel(); + _reconnectAttempts = 0; + _reconnectTimer = Timer.periodic(_reconnectInterval, (timer) async { + if (_reconnectAttempts >= _maxReconnectAttempts) { + debugPrint('BluetoothService: Max reconnect attempts reached'); + timer.cancel(); + return; + } + if (isConnected) { + timer.cancel(); + return; + } + + _reconnectAttempts++; + debugPrint( + 'BluetoothService: Reconnect attempt $_reconnectAttempts/$_maxReconnectAttempts'); + final ok = await _connectToAddress(_deviceAddress); + if (ok) timer.cancel(); + }); + } + + /// Manual reconnect trigger (e.g. from a "Reconnect" button). + Future manualReconnect() async { + if (_deviceAddress.isEmpty) return false; + _userDisconnected = false; + _reconnectAttempts = 0; + return _connectToAddress(_deviceAddress); + } + + // ══════════════════════════════════════════════════════════ + // DATA PARSING + // ══════════════════════════════════════════════════════════ + + void _handleData(Uint8List data) { + _buffer += utf8.decode(data, allowMalformed: true); + + while (_buffer.contains('\n')) { + final idx = _buffer.indexOf('\n'); + final line = _buffer.substring(0, idx).trim(); + _buffer = _buffer.substring(idx + 1); + if (line.isEmpty) continue; + + debugPrint('BluetoothService: β—€ "$line"'); + + BraceletCommand? cmd; + final upper = line.toUpperCase(); + if (upper.contains('BUTTON SOS') || upper.contains('MOTION SOS') || upper == 'SOS') { + cmd = BraceletCommand.sos; + } else if (upper.contains('SHAKE')) { + cmd = BraceletCommand.shake; + } else if (upper.contains('VOICE')) { + cmd = BraceletCommand.voice; + } else if (upper.contains('CAM')) { + cmd = BraceletCommand.camera; + } + + if (cmd != null) { + debugPrint('BluetoothService: 🚨 Command β†’ $cmd'); + onCommand?.call(cmd); + } + } + } + + // ══════════════════════════════════════════════════════════ + // SEND COMMANDS TO ESP32 + // ══════════════════════════════════════════════════════════ + + Future send(String command) async { + if (_connection == null || !isConnected) return; + try { + _connection!.output.add(Uint8List.fromList(utf8.encode('$command\n'))); + await _connection!.output.allSent; + debugPrint('BluetoothService: Sent "$command"'); + } catch (e) { + debugPrint('BluetoothService: Write failed β€” $e'); + } + } + + Future activateBuzzer() => send('BUZZER_ON'); + Future stopBuzzer() => send('BUZZER_OFF'); + Future ledOn() => send('LED_ON'); + Future ledOff() => send('LED_OFF'); + + // ══════════════════════════════════════════════════════════ + // SIMULATE (for demos without hardware) + // ══════════════════════════════════════════════════════════ + + void simulateCommand(BraceletCommand command) { + debugPrint('BluetoothService: Simulated $command'); + onCommand?.call(command); + } + + // ══════════════════════════════════════════════════════════ + // HELPERS + // ══════════════════════════════════════════════════════════ + + void _setState(BtConnectionState s) { + if (_state == s) return; + _state = s; + notifyListeners(); + } + + /// Clear saved device (e.g. user wants to forget the device). + Future forgetDevice() async { + disconnect(); + _deviceName = ''; + _deviceAddress = ''; + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_kDeviceName); + await prefs.remove(_kDeviceAddress); + notifyListeners(); + } +} diff --git a/Team-Shivam/SheShield/lib/services/bracelet_service.dart b/Team-Shivam/SheShield/lib/services/bracelet_service.dart new file mode 100644 index 0000000..ab45c8d --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/bracelet_service.dart @@ -0,0 +1,147 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bluetooth_serial_ble/flutter_bluetooth_serial_ble.dart'; + +/// Commands from the ESP32 bracelet. +enum BraceletCommand { sos, shake, voice, camera } + +typedef BraceletCommandCallback = void Function(BraceletCommand command); + +/// **Global singleton** Bluetooth Classic service. +/// +/// Connection persists across all screens. Only disconnects +/// when the user explicitly taps "Disconnect". +/// +/// ESP32 sends: "SOS\n", "SHAKE\n", "BUTTON SOS\n", "MOTION SOS\n" +/// App sends: "BUZZER_ON\n", "LED_ON\n", etc. +class BraceletService extends ChangeNotifier { + // ── Singleton ── + BraceletService._(); + static final BraceletService instance = BraceletService._(); + + BluetoothConnection? _connection; + StreamSubscription? _inputSub; + String _buffer = ''; + + // ── Public state ── + bool _isConnected = false; + String _deviceName = ''; + String _deviceAddress = ''; + + bool get isConnected => _isConnected; + String get deviceName => _deviceName; + String get deviceAddress => _deviceAddress; + + /// Global callback β€” fires from ANY screen when SOS received. + BraceletCommandCallback? onCommand; + + /// Connect to a device via RFCOMM and start listening. + Future connect(BluetoothDevice device) async { + // Already connected to this device + if (_isConnected && _deviceAddress == device.address) return true; + + // Disconnect previous if any + if (_isConnected) disconnect(); + + _deviceName = device.name ?? device.address; + _deviceAddress = device.address; + + try { + debugPrint('BraceletService: Connecting to $_deviceName…'); + _connection = await BluetoothConnection.toAddress(device.address); + debugPrint('BraceletService: βœ… Connected to $_deviceName'); + + _inputSub?.cancel(); + _inputSub = _connection!.input?.listen( + _handleData, + onDone: () { + debugPrint('BraceletService: Connection closed by remote'); + _setDisconnected(); + }, + onError: (e) { + debugPrint('BraceletService: Stream error β€” $e'); + _setDisconnected(); + }, + ); + + _isConnected = true; + notifyListeners(); + return true; + } catch (e) { + debugPrint('BraceletService: Connection failed β€” $e'); + _setDisconnected(); + return false; + } + } + + /// Explicitly disconnect (user tapped "Disconnect"). + void disconnect() { + debugPrint('BraceletService: Disconnecting…'); + _inputSub?.cancel(); + _inputSub = null; + try { _connection?.finish(); } catch (_) {} + _connection = null; + _setDisconnected(); + } + + void _setDisconnected() { + _isConnected = false; + _buffer = ''; + notifyListeners(); + } + + /// Send a string command to ESP32. + Future send(String command) async { + if (_connection == null || !_isConnected) return; + try { + _connection!.output.add(Uint8List.fromList(utf8.encode('$command\n'))); + await _connection!.output.allSent; + debugPrint('BraceletService: Sent "$command"'); + } catch (e) { + debugPrint('BraceletService: Write failed β€” $e'); + } + } + + Future activateBuzzer() => send('BUZZER_ON'); + Future stopBuzzer() => send('BUZZER_OFF'); + Future ledOn() => send('LED_ON'); + Future ledOff() => send('LED_OFF'); + + /// Parse incoming serial data line-by-line. + void _handleData(Uint8List data) { + _buffer += utf8.decode(data, allowMalformed: true); + + while (_buffer.contains('\n')) { + final idx = _buffer.indexOf('\n'); + final line = _buffer.substring(0, idx).trim(); + _buffer = _buffer.substring(idx + 1); + if (line.isEmpty) continue; + + debugPrint('BraceletService: β—€ "$line"'); + + BraceletCommand? cmd; + final upper = line.toUpperCase(); + if (upper.contains('SOS')) { + cmd = BraceletCommand.sos; + } else if (upper.contains('SHAKE')) { + cmd = BraceletCommand.shake; + } else if (upper.contains('VOICE')) { + cmd = BraceletCommand.voice; + } else if (upper.contains('CAM')) { + cmd = BraceletCommand.camera; + } + + if (cmd != null) { + debugPrint('BraceletService: 🚨 Command β†’ $cmd'); + onCommand?.call(cmd); + } + } + } + + /// For demo: fire a command without a real device. + void simulateCommand(BraceletCommand command) { + debugPrint('BraceletService: Simulated $command'); + onCommand?.call(command); + } +} diff --git a/Team-Shivam/SheShield/lib/services/location_service.dart b/Team-Shivam/SheShield/lib/services/location_service.dart new file mode 100644 index 0000000..6af23ed --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/location_service.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'package:geolocator/geolocator.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +/// Service wrapping the Geolocator package for GPS location access +/// and Firebase Firestore for uploading location updates. +/// +/// Provides SOS-triggered live tracking that writes the user's GPS +/// coordinates to the `live_locations` Firestore collection every +/// 5 seconds. +class LocationService { + static Timer? _sosTimer; + static bool _tracking = false; + + /// Whether the SOS live-tracking loop is currently running. + static bool get isTracking => _tracking; + + // --------------------------------------------------------------------------- + // Permission & single-shot location + // --------------------------------------------------------------------------- + + /// Check and request location permissions, then get current position. + static Future getCurrentLocation() async { + await _ensurePermissions(); + + return await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + ); + } + + // --------------------------------------------------------------------------- + // Continuous stream (used by LocationScreen / Google-Maps view) + // --------------------------------------------------------------------------- + + /// Stream of position updates (every ~5 meters of movement). + static Stream getLocationStream() { + return Geolocator.getPositionStream( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 5, + ), +); + } + + // --------------------------------------------------------------------------- + // SOS live-tracking (Timer-based, every 5 seconds) + // --------------------------------------------------------------------------- + + /// Start SOS live-location tracking. + /// + /// 1. Ensures location permissions are granted. + /// 2. Fetches the current position immediately and writes it to Firestore. + /// 3. Starts a periodic timer that fetches and writes the position every + /// 5 seconds until [stopSOS] is called. + static Future startSOS(String userId) async { + if (_tracking) return; // already running + + await _ensurePermissions(); + + // Write the first location immediately. + await _fetchAndWrite(userId); + + // Schedule subsequent writes every 5 seconds. + _tracking = true; + _sosTimer = Timer.periodic(const Duration(seconds: 5), (_) { + _fetchAndWrite(userId); + }); + } + + /// Stop SOS live-location tracking. + static void stopSOS() { + _sosTimer?.cancel(); + _sosTimer = null; + _tracking = false; + } + + // --------------------------------------------------------------------------- + // Firestore helpers + // --------------------------------------------------------------------------- + + /// Fetch the current position and write it to the `live_locations` collection. + static Future _fetchAndWrite(String userId) async { + try { + final pos = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + ); + await _writeToFirestore(userId, pos.latitude, pos.longitude); + } catch (_) { + // Silently ignore individual fetch/write failures so the timer + // keeps running and retries on the next tick. + } + } + + /// Write a single location document to Firestore. + static Future _writeToFirestore( + String userId, + double lat, + double lng, + ) async { + await FirebaseFirestore.instance.collection('live_locations').add({ + 'userId': userId, + 'latitude': lat, + 'longitude': lng, + 'timestamp': FieldValue.serverTimestamp(), + }); + } + + /// Send the current latitude and longitude to Firestore (`live_locations`). + static Future sendLocationToFirestore( + double lat, + double lng, { + String userId = 'user_placeholder', + }) async { + await _writeToFirestore(userId, lat, lng); + } + + // --------------------------------------------------------------------------- + // Permission helper + // --------------------------------------------------------------------------- + + /// Ensures location services are enabled and permissions are granted. + /// Throws an [Exception] if the user denies permission or services are off. + static Future _ensurePermissions() async { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + throw Exception('Location services are disabled. Please enable them.'); + } + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + throw Exception('Location permission denied.'); + } + } + + if (permission == LocationPermission.deniedForever) { + throw Exception( + 'Location permissions are permanently denied. ' + 'Please enable them in settings.', + ); + } + } +} diff --git a/Team-Shivam/SheShield/lib/services/notification_service.dart b/Team-Shivam/SheShield/lib/services/notification_service.dart new file mode 100644 index 0000000..90279c0 --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/notification_service.dart @@ -0,0 +1,59 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +/// Service for handling push notifications and Firestore alert storage. +class NotificationService { + static final FirebaseMessaging _messaging = FirebaseMessaging.instance; + static final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + /// Cached FCM device token. + static String? _fcmToken; + + /// Initialize FCM: request notification permissions and retrieve the + /// device token. Call this once at app startup after [Firebase.initializeApp]. + static Future initialize() async { + // Request notification permissions (shows the system dialog on first launch). + final settings = await _messaging.requestPermission( + alert: true, + badge: true, + sound: true, + provisional: false, + ); + + if (settings.authorizationStatus == AuthorizationStatus.authorized || + settings.authorizationStatus == AuthorizationStatus.provisional) { + // Retrieve the FCM token for this device. + _fcmToken = await _messaging.getToken(); + } + + // Listen for foreground messages so they can be handled in-app. + FirebaseMessaging.onMessage.listen(_handleForegroundMessage); + } + + /// Handle push notifications received while the app is in the foreground. + static void _handleForegroundMessage(RemoteMessage message) { + // Foreground messages are silently received; the SOS dialog already + // provides visual feedback, so no additional UI action is needed here. + } + + /// Send an SOS alert: saves the alert to the Firestore "alerts" collection + /// and returns the document ID. + /// + /// [lat] and [lng] are the user's current GPS coordinates. + static Future sendSOSAlert({double? lat, double? lng}) async { + try { + final docRef = await _firestore.collection('alerts').add({ + 'title': 'SOS Alert', + 'body': 'User has triggered an emergency alert.', + 'latitude': lat, + 'longitude': lng, + 'fcmToken': _fcmToken, + 'timestamp': FieldValue.serverTimestamp(), + }); + return docRef.id; + } catch (e) { + // Silently fail – the SOS UI feedback is already visible to the user. + return null; + } + } +} diff --git a/Team-Shivam/SheShield/lib/services/places_service.dart b/Team-Shivam/SheShield/lib/services/places_service.dart new file mode 100644 index 0000000..d443421 --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/places_service.dart @@ -0,0 +1,215 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:geolocator/geolocator.dart'; + +/// Model representing a nearby police station. +class PoliceStation { + final String name; + final String vicinity; + final double lat; + final double lng; + final String placeId; + final double distanceMeters; + + PoliceStation({ + required this.name, + required this.vicinity, + required this.lat, + required this.lng, + required this.placeId, + required this.distanceMeters, + }); + + /// Human-readable distance string. + String get distanceText { + if (distanceMeters >= 1000) { + return '${(distanceMeters / 1000).toStringAsFixed(1)} km'; + } + return '${distanceMeters.toInt()} m'; + } +} + +/// Service for fetching nearby police stations. +/// Races multiple APIs in parallel and returns the first successful response. +class PlacesService { + /// Fetch police stations near the given coordinates. + /// Fires ALL APIs simultaneously, merges results, deduplicates, + /// and returns them sorted by distance (nearest first). + static Future> fetchNearbyPoliceStations( + double lat, + double lng, { + int radiusMeters = 100000, + }) async { + // Fire all sources in parallel and collect all results. + final futures = await Future.wait>([ + _fetchFromOverpass( + 'https://overpass-api.de/api/interpreter', lat, lng, radiusMeters, + ).catchError((_) => []), + _fetchFromOverpass( + 'https://overpass.kumi.systems/api/interpreter', lat, lng, radiusMeters, + ).catchError((_) => []), + _fetchFromNominatim(lat, lng) + .catchError((_) => []), + ]).timeout( + const Duration(seconds: 12), + onTimeout: () => [[], [], []], + ); + + // Merge all results into one list. + final allStations = []; + for (final list in futures) { + allStations.addAll(list); + } + + // Deduplicate stations that are within 100m of each other. + final unique = []; + for (final station in allStations) { + final isDuplicate = unique.any((existing) { + final dist = Geolocator.distanceBetween( + existing.lat, existing.lng, station.lat, station.lng, + ); + return dist < 100; // within 100m = same station + }); + if (!isDuplicate) { + unique.add(station); + } + } + + // Sort by distance ascending (nearest first). + unique.sort((a, b) => a.distanceMeters.compareTo(b.distanceMeters)); + + return unique; + } + + // ------------------------------------------------------------------------- + // Overpass API + // ------------------------------------------------------------------------- + static Future> _fetchFromOverpass( + String endpoint, + double lat, + double lng, + int radiusMeters, + ) async { + final query = '[out:json][timeout:8];' + 'node["amenity"="police"](around:$radiusMeters,$lat,$lng);' + 'out body;'; + + final uri = Uri.parse(endpoint); + final response = await http + .post(uri, body: {'data': query}) + .timeout(const Duration(seconds: 8)); + + if (response.statusCode != 200) { + throw Exception('Overpass returned ${response.statusCode}'); + } + + final data = jsonDecode(response.body) as Map; + final elements = data['elements'] as List? ?? []; + + final stations = []; + for (final el in elements) { + final stationLat = (el['lat'] as num?)?.toDouble(); + final stationLng = (el['lon'] as num?)?.toDouble(); + if (stationLat == null || stationLng == null) continue; + + final tags = el['tags'] as Map? ?? {}; + final name = tags['name'] as String? ?? 'Police Station'; + final address = tags['addr:full'] as String? ?? + tags['addr:street'] as String? ?? + ''; + + final distance = Geolocator.distanceBetween( + lat, lng, stationLat, stationLng, + ); + + stations.add(PoliceStation( + name: name, + vicinity: address, + lat: stationLat, + lng: stationLng, + placeId: el['id'].toString(), + distanceMeters: distance, + )); + } + + stations.sort((a, b) => a.distanceMeters.compareTo(b.distanceMeters)); + return stations; + } + + // ------------------------------------------------------------------------- + // Nominatim fallback + // ------------------------------------------------------------------------- + static Future> _fetchFromNominatim( + double lat, + double lng, + ) async { + final uri = Uri.parse( + 'https://nominatim.openstreetmap.org/search' + '?q=police+station' + '&format=json' + '&limit=30' + '&viewbox=${lng - 0.1},${lat + 0.1},${lng + 0.1},${lat - 0.1}' + '&bounded=1' + '&addressdetails=1', + ); + + final response = await http.get( + uri, + headers: {'User-Agent': 'SheShield-App/1.0'}, + ).timeout(const Duration(seconds: 8)); + + if (response.statusCode != 200) { + throw Exception('Nominatim returned ${response.statusCode}'); + } + + final results = jsonDecode(response.body) as List; + final stations = []; + + for (final r in results) { + final stationLat = double.tryParse(r['lat']?.toString() ?? ''); + final stationLng = double.tryParse(r['lon']?.toString() ?? ''); + if (stationLat == null || stationLng == null) continue; + + final name = + r['display_name']?.toString().split(',').first ?? 'Police Station'; + final address = r['display_name']?.toString() ?? ''; + + final distance = Geolocator.distanceBetween( + lat, lng, stationLat, stationLng, + ); + + stations.add(PoliceStation( + name: name, + vicinity: + address.length > 60 ? '${address.substring(0, 57)}...' : address, + lat: stationLat, + lng: stationLng, + placeId: r['place_id']?.toString() ?? '', + distanceMeters: distance, + )); + } + + stations.sort((a, b) => a.distanceMeters.compareTo(b.distanceMeters)); + return stations; + } + + // ------------------------------------------------------------------------- + // Navigation URL helpers + // ------------------------------------------------------------------------- + + /// Generate a Google Maps navigation deep-link for a destination. + static String getNavigationUrl(double destLat, double destLng) { + return 'google.navigation:q=$destLat,$destLng&mode=d'; + } + + /// Generate a Google Maps directions web URL. + static String getDirectionsUrl(double destLat, double destLng) { + return 'https://www.google.com/maps/dir/?api=1&destination=$destLat,$destLng&travelmode=driving'; + } + + /// Generate a Google Maps web URL (fallback for devices without Google Maps). + static String getMapsUrl(double destLat, double destLng) { + return 'https://www.google.com/maps/dir/?api=1&destination=$destLat,$destLng&travelmode=driving'; + } +} diff --git a/Team-Shivam/SheShield/lib/services/risk_assessment_service.dart b/Team-Shivam/SheShield/lib/services/risk_assessment_service.dart new file mode 100644 index 0000000..a022016 --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/risk_assessment_service.dart @@ -0,0 +1,406 @@ +import 'dart:math'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:http/http.dart' as http; + +// ═══════════════════════════════════════════════════════════════════ +// Risk Assessment Service – OFFLINE-FIRST + ONLINE-ENHANCED +// +// Offline mode (default): +// β€’ Time-based check only (night = risky) +// β€’ Uses hardcoded safe-zone list for nearest safe zone display +// +// Online mode (when internet is available): +// β€’ Fetches nearby places via Overpass API +// β€’ If <= 2 places found nearby β†’ isolated area +// β€’ Falls back to offline mode on API failure +// ═══════════════════════════════════════════════════════════════════ + +/// Risk severity levels β€” ordered from safest to most dangerous. +enum RiskLevel { safe, moderate, high, critical } + +/// Whether online-enhanced detection was used. +enum DetectionMode { offline, online } + +/// Info about the closest safe zone to the user. +class SafeZoneInfo { + final String name; + final String type; + final double distanceKm; + final double lat; + final double lng; + + const SafeZoneInfo({ + required this.name, + required this.type, + required this.distanceKm, + required this.lat, + required this.lng, + }); + + String get displayText => '$name (${distanceKm.toStringAsFixed(1)} km)'; +} + +/// Holds the result of a risk assessment. +class RiskResult { + final RiskLevel level; + final String message; + final String subtitle; + final List reasons; + final SafeZoneInfo? nearestSafeZone; + final DetectionMode mode; + final int? nearbyPlacesCount; + final int riskScore; // 0=safe, 1=moderate, 2=high, 3=critical + + const RiskResult({ + required this.level, + required this.message, + required this.subtitle, + required this.reasons, + this.nearestSafeZone, + required this.mode, + this.nearbyPlacesCount, + required this.riskScore, + }); +} + +/// Pure-logic service β€” offline-first with online enhancement. +class RiskAssessmentService { + // ──────────────────────────────────────────────────────────────── + // Demo toggle β€” can still be flipped from the UI switch. + // ──────────────────────────────────────────────────────────────── + static bool isIsolatedArea = false; + + // ──────────────────────────────────────────────────────────────── + // Named safe zones (offline fallback for nearest safe zone) + // ──────────────────────────────────────────────────────────────── + static const List<_SafePlace> _safePlaces = [ + _SafePlace('Parliament St Police Station', 'Police Station', 28.6218, 77.2120), + _SafePlace('AIIMS Hospital', 'Hospital', 28.5672, 77.2100), + _SafePlace('Connaught Place', 'Public Area', 28.6315, 77.2167), + _SafePlace('Safdarjung Hospital', 'Hospital', 28.5685, 77.2066), + _SafePlace('Noida Sec 18 Market', 'Public Area', 28.5706, 77.3218), + _SafePlace('CST Railway Station', 'Public Area', 19.0826, 72.8853), + _SafePlace('JJ Hospital Mumbai', 'Hospital', 18.9634, 72.8330), + _SafePlace('Colaba Police Station', 'Police Station', 18.9220, 72.8347), + _SafePlace('MG Road Metro', 'Public Area', 12.9758, 77.6065), + _SafePlace('Victoria Hospital', 'Hospital', 12.9563, 77.5737), + _SafePlace('Cubbon Park Police', 'Police Station', 12.9763, 77.5929), + _SafePlace('Chennai Central', 'Public Area', 13.0836, 80.2750), + _SafePlace('Rajiv Gandhi Hospital', 'Hospital', 13.0700, 80.2800), + _SafePlace('Park Street', 'Public Area', 22.5534, 88.3535), + _SafePlace('SSKM Hospital Kolkata', 'Hospital', 22.5383, 88.3436), + _SafePlace('HITEC City', 'Public Area', 17.4435, 78.3772), + _SafePlace('Nizam\'s Hospital', 'Hospital', 17.3887, 78.4741), + _SafePlace('Hawa Mahal Area', 'Public Area', 26.9239, 75.8267), + _SafePlace('SMS Hospital Jaipur', 'Hospital', 26.9038, 75.8013), + _SafePlace('SG Highway', 'Public Area', 23.0300, 72.5300), + _SafePlace('Civil Hospital Ahmedabad', 'Hospital', 23.0500, 72.6000), + _SafePlace('Cyber Hub Gurgaon', 'Public Area', 28.4949, 77.0887), + _SafePlace('Medanta Hospital', 'Hospital', 28.4395, 77.0425), + ]; + + /// Minimum places needed to consider an area "populated". + static const int _minPlacesForSafe = 5; + + // ──────────────────────────────────────────────────────────────── + // Public helpers + // ──────────────────────────────────────────────────────────────── + + static bool isNightTime() => + DateTime.now().hour >= 21 || DateTime.now().hour < 5; + + /// Check if the device has internet connectivity. + static Future _hasInternet() async { + try { + final result = await Connectivity().checkConnectivity(); + return !result.contains(ConnectivityResult.none); + } catch (_) { + return false; + } + } + + // ──────────────────────────────────────────────────────────────── + // Main entry-point (async β€” checks internet first) + // ──────────────────────────────────────────────────────────────── + + /// Assess risk with online enhancement when internet is available. + /// Falls back to offline-only when no internet or API fails. + static Future assessRiskAsync(double lat, double lng) async { + final online = await _hasInternet(); + + if (online) { + try { + return await _assessOnline(lat, lng); + } catch (e) { + debugPrint('RiskAssessment: Online check failed, falling back β€” $e'); + return _assessOffline(lat, lng); + } + } else { + return _assessOffline(lat, lng); + } + } + + /// Synchronous offline-only assessment (instant, no network). + static RiskResult assessRisk(double lat, double lng) { + return _assessOffline(lat, lng); + } + + // ──────────────────────────────────────────────────────────────── + // Online assessment (Overpass API) + // ──────────────────────────────────────────────────────────────── + + static Future _assessOnline(double lat, double lng) async { + final reasons = []; + final hour = DateTime.now().hour; + final nightTime = isNightTime(); + + if (nightTime) { + reasons.add('It\'s late night (${_formatHour(hour)})'); + } + + // Fetch nearby places (amenities) within 1000m radius via Overpass + final placesCount = await _fetchNearbyPlacesCount(lat, lng); + final isolated = isIsolatedArea || placesCount < _minPlacesForSafe; + + // Debug logs + debugPrint('πŸ” [ONLINE] Places count: $placesCount'); + debugPrint('πŸ” [ONLINE] isIsolatedArea (demo toggle): $isIsolatedArea'); + debugPrint('πŸ” [ONLINE] isolated (final): $isolated'); + + if (isolated) { + reasons.add('Only $placesCount places found nearby'); + } + + // Calculate risk score + int riskScore = 0; + if (nightTime) riskScore += 1; + if (isolated) riskScore += 2; + + debugPrint('πŸ” [ONLINE] riskScore: $riskScore (night=$nightTime, isolated=$isolated)'); + + // Find nearest safe zone from our hardcoded list + final nearest = _findNearestSafePlace(lat, lng); + + return _buildResult( + riskScore: riskScore, + reasons: reasons, + nearest: nearest, + mode: DetectionMode.online, + placesCount: placesCount, + ); + } + + /// Query Overpass API for nearby amenities within 1000m. + /// Returns the count of results found. + static Future _fetchNearbyPlacesCount(double lat, double lng) async { + // Search for ANY amenity, shop, or leisure node within 1000m + // No type filter β€” broad query returns more results in crowded areas + final query = '[out:json][timeout:8];' + '(' + 'node["amenity"](around:1000,$lat,$lng);' + 'node["shop"](around:1000,$lat,$lng);' + 'node["leisure"](around:1000,$lat,$lng);' + ')' + ';out count;'; + + try { + final response = await http + .post( + Uri.parse('https://overpass-api.de/api/interpreter'), + body: {'data': query}, + ) + .timeout(const Duration(seconds: 8)); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final elements = data['elements'] as List? ?? []; + // "out count" returns a single element with a "tags" map containing "total" + if (elements.isNotEmpty && elements[0]['tags'] != null) { + final total = int.tryParse( + elements[0]['tags']['total']?.toString() ?? '0'); + debugPrint('πŸ” Overpass API returned: $total places within 1000m'); + return total ?? 0; + } + debugPrint('πŸ” Overpass API returned: ${elements.length} elements'); + return elements.length; + } + debugPrint('πŸ” Overpass API status: ${response.statusCode}'); + } catch (e) { + debugPrint('πŸ” Overpass query failed β€” $e'); + } + // FAIL-SAFE: On failure, assume safe (return high count to avoid false alerts) + debugPrint('πŸ” API failed β†’ assuming safe (returning 99)'); + return 99; + } + + // ──────────────────────────────────────────────────────────────── + // Offline assessment (local data only, instant) + // ──────────────────────────────────────────────────────────────── + + static RiskResult _assessOffline(double lat, double lng) { + final reasons = []; + final hour = DateTime.now().hour; + final nightTime = isNightTime(); + + if (nightTime) { + reasons.add('It\'s late night (${_formatHour(hour)})'); + } + + // In offline mode, use demo toggle or GPS distance for isolation + // Use 5km threshold (more lenient to avoid false positives) + final nearestKm = _distanceToNearestSafeZone(lat, lng); + final isolated = isIsolatedArea || nearestKm > 5.0; + + // Debug logs + debugPrint('πŸ” [OFFLINE] Nearest safe zone: ${nearestKm.toStringAsFixed(1)} km'); + debugPrint('πŸ” [OFFLINE] isIsolatedArea (demo toggle): $isIsolatedArea'); + debugPrint('πŸ” [OFFLINE] isolated (final): $isolated'); + + if (isolated) { + reasons.add('${nearestKm.toStringAsFixed(1)} km from nearest safe zone'); + } + + // Calculate risk score + int riskScore = 0; + if (nightTime) riskScore += 1; + if (isolated) riskScore += 2; + + debugPrint('πŸ” [OFFLINE] riskScore: $riskScore (night=$nightTime, isolated=$isolated)'); + + final nearest = _findNearestSafePlace(lat, lng); + + return _buildResult( + riskScore: riskScore, + reasons: reasons, + nearest: nearest, + mode: DetectionMode.offline, + ); + } + + // ──────────────────────────────────────────────────────────────── + // Shared result builder + // ──────────────────────────────────────────────────────────────── + + static RiskResult _buildResult({ + required int riskScore, + required List reasons, + SafeZoneInfo? nearest, + required DetectionMode mode, + int? placesCount, + }) { + // Score 0 = safe, 1 = moderate, 2 = high, 3 = critical + if (riskScore >= 3) { + return RiskResult( + level: RiskLevel.critical, + message: '🚨 Critical Risk: Immediate Action Needed', + subtitle: 'Late night + low activity area β€” stay alert', + reasons: reasons, + nearestSafeZone: nearest, + mode: mode, + nearbyPlacesCount: placesCount, + riskScore: riskScore, + ); + } else if (riskScore == 2) { + return RiskResult( + level: RiskLevel.high, + message: '🚨 High Risk: Low Activity Area', + subtitle: 'You appear to be in an isolated location', + reasons: reasons, + nearestSafeZone: nearest, + mode: mode, + nearbyPlacesCount: placesCount, + riskScore: riskScore, + ); + } else if (riskScore == 1) { + return RiskResult( + level: RiskLevel.moderate, + message: '⚠️ Be Alert: Late Night', + subtitle: 'Late-night hours β€” stay in well-lit areas', + reasons: reasons, + nearestSafeZone: nearest, + mode: mode, + nearbyPlacesCount: placesCount, + riskScore: riskScore, + ); + } else { + return RiskResult( + level: RiskLevel.safe, + message: 'βœ… You are in a safe area', + subtitle: 'No risk factors detected right now', + reasons: reasons, + nearestSafeZone: nearest, + mode: mode, + nearbyPlacesCount: placesCount, + riskScore: riskScore, + ); + } + } + + // ──────────────────────────────────────────────────────────────── + // Helpers + // ──────────────────────────────────────────────────────────────── + + static SafeZoneInfo? _findNearestSafePlace(double lat, double lng) { + if (_safePlaces.isEmpty) return null; + _SafePlace? best; + double bestDist = double.infinity; + for (final p in _safePlaces) { + final d = _haversineKm(lat, lng, p.lat, p.lng); + if (d < bestDist) { + bestDist = d; + best = p; + } + } + if (best == null) return null; + return SafeZoneInfo( + name: best.name, + type: best.type, + distanceKm: bestDist, + lat: best.lat, + lng: best.lng, + ); + } + + static double _distanceToNearestSafeZone(double lat, double lng) { + double nearest = double.infinity; + for (final zone in _safePlaces) { + final d = _haversineKm(lat, lng, zone.lat, zone.lng); + if (d < nearest) nearest = d; + } + return nearest; + } + + static double _haversineKm( + double lat1, double lng1, + double lat2, double lng2, + ) { + const earthRadiusKm = 6371.0; + final dLat = _degToRad(lat2 - lat1); + final dLng = _degToRad(lng2 - lng1); + final a = sin(dLat / 2) * sin(dLat / 2) + + cos(_degToRad(lat1)) * + cos(_degToRad(lat2)) * + sin(dLng / 2) * + sin(dLng / 2); + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + return earthRadiusKm * c; + } + + static double _degToRad(double deg) => deg * (pi / 180); + + static String _formatHour(int hour) { + final h = hour % 12 == 0 ? 12 : hour % 12; + final period = hour >= 12 ? 'PM' : 'AM'; + return '$h $period'; + } +} + +class _SafePlace { + final String name; + final String type; + final double lat; + final double lng; + const _SafePlace(this.name, this.type, this.lat, this.lng); +} diff --git a/Team-Shivam/SheShield/lib/services/sms_service.dart b/Team-Shivam/SheShield/lib/services/sms_service.dart new file mode 100644 index 0000000..c27fc1c --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/sms_service.dart @@ -0,0 +1,85 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'storage_service.dart'; + +/// Service for sending SMS directly from the app (no user interaction needed). +/// Uses a platform channel to call Android's native SmsManager. +class SmsService { + static const _channel = MethodChannel('com.sheshield/sms'); + + /// Sends a raw SMS to the given phone number. + static Future _sendSms(String phoneNumber, String message) async { + try { + await _channel.invokeMethod('sendSms', { + 'phone': phoneNumber, + 'message': message, + }); + return true; + } catch (e) { + debugPrint('SmsService: Failed to send SMS to $phoneNumber β€” $e'); + return false; + } + } + + /// Requests SMS permission and sends an SMS directly to [phoneNumber] + /// notifying them that they have been added as an emergency contact. + static Future sendContactAddedSms({ + required String phoneNumber, + required String contactName, + }) async { + final status = await Permission.sms.request(); + if (!status.isGranted) { + debugPrint('SmsService: SMS permission denied'); + return false; + } + + final userName = + FirebaseAuth.instance.currentUser?.displayName ?? 'a SheShield user'; + + final message = + 'Hi $contactName! You have been added as an emergency contact on ' + 'SheShield by $userName. In case of an emergency, you will ' + 'receive SOS alerts with a live location link. Please stay reachable. ' + '- Sent via SheShield'; + + return _sendSms(phoneNumber, message); + } + + /// Sends an SOS emergency SMS to ALL saved emergency contacts with location. + static Future sendSOSToAllContacts({ + required double lat, + required double lng, + }) async { + final status = await Permission.sms.request(); + if (!status.isGranted) { + debugPrint('SmsService: SMS permission denied β€” cannot send SOS'); + return; + } + + final contacts = await StorageService.loadContacts(); + if (contacts.isEmpty) { + debugPrint('SmsService: No emergency contacts to send SOS to'); + return; + } + + final userName = + FirebaseAuth.instance.currentUser?.displayName ?? 'A SheShield user'; + + final mapLink = 'https://www.google.com/maps?q=$lat,$lng'; + + final message = + '🚨 SOS EMERGENCY ALERT!\n\n' + '$userName has triggered an emergency SOS on SheShield.\n\n' + 'πŸ“ Live Location: $mapLink\n\n' + 'Please respond immediately or contact emergency services. ' + '- SheShield Emergency Alert'; + + for (final contact in contacts) { + await _sendSms(contact.phone, message); + } + + debugPrint('SmsService: SOS SMS sent to ${contacts.length} contacts'); + } +} diff --git a/Team-Shivam/SheShield/lib/services/storage_service.dart b/Team-Shivam/SheShield/lib/services/storage_service.dart new file mode 100644 index 0000000..134684c --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/storage_service.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Represents an emergency contact. +class Contact { + final String id; + final String name; + final String phone; + + Contact({ + required this.id, + required this.name, + required this.phone, + }); + + Map toJson() => { + 'id': id, + 'name': name, + 'phone': phone, + }; + + factory Contact.fromJson(Map json) => Contact( + id: json['id'] as String, + name: json['name'] as String, + phone: json['phone'] as String, + ); +} + +/// Service for persisting emergency contacts using SharedPreferences. +class StorageService { + static const String _contactsKey = 'sheshield_contacts'; + + /// Load all saved contacts. + static Future> loadContacts() async { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = prefs.getString(_contactsKey); + if (jsonStr == null || jsonStr.isEmpty) { + // Return default sample contacts on first launch + final defaults = [ + Contact(id: '1', name: 'Mom', phone: '+91 98765 43210'), + Contact(id: '2', name: 'Dad', phone: '+91 98765 43211'), + Contact(id: '3', name: 'Sister', phone: '+91 98765 43212'), + ]; + await saveContacts(defaults); + return defaults; + } + final List list = jsonDecode(jsonStr); + return list.map((e) => Contact.fromJson(e as Map)).toList(); + } + + /// Save contacts list. + static Future saveContacts(List contacts) async { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = jsonEncode(contacts.map((c) => c.toJson()).toList()); + await prefs.setString(_contactsKey, jsonStr); + } + + /// Add a new contact. + static Future> addContact( + List current, String name, String phone) async { + final contact = Contact( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: name, + phone: phone, + ); + final updated = [...current, contact]; + await saveContacts(updated); + return updated; + } + + /// Delete a contact by ID. + static Future> deleteContact( + List current, String id) async { + final updated = current.where((c) => c.id != id).toList(); + await saveContacts(updated); + return updated; + } +} diff --git a/Team-Shivam/SheShield/lib/services/video_recording_service.dart b/Team-Shivam/SheShield/lib/services/video_recording_service.dart new file mode 100644 index 0000000..09ad601 --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/video_recording_service.dart @@ -0,0 +1,176 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:camera/camera.dart'; +import 'package:flutter/foundation.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:path_provider/path_provider.dart'; + +/// Service that records a 30-second video when SOS is triggered +/// and uploads it to Firebase Storage under `sos_recordings/`. +class VideoRecordingService { + static CameraController? _controller; + static bool _isRecording = false; + static Timer? _stopTimer; + + /// Start recording a 30-second video using the rear camera. + /// The video is automatically stopped and uploaded after 30 seconds. + static Future startSOSRecording() async { + if (_isRecording) return; + + try { + // Request camera and microphone permissions + final cameraStatus = await Permission.camera.request(); + final micStatus = await Permission.microphone.request(); + + if (!cameraStatus.isGranted || !micStatus.isGranted) { + debugPrint('VideoRecordingService: Camera/mic permission denied'); + return; + } + + // Get available cameras + final cameras = await availableCameras(); + if (cameras.isEmpty) { + debugPrint('VideoRecordingService: No cameras available'); + return; + } + + // Prefer back camera for evidence recording + final camera = cameras.firstWhere( + (c) => c.lensDirection == CameraLensDirection.back, + orElse: () => cameras.first, + ); + + // Initialize camera controller + _controller = CameraController( + camera, + ResolutionPreset.medium, + enableAudio: true, + ); + + await _controller!.initialize(); + + // Start recording + await _controller!.startVideoRecording(); + _isRecording = true; + debugPrint('VideoRecordingService: Recording started'); + + // Auto-stop after 30 seconds + _stopTimer = Timer(const Duration(seconds: 30), () { + stopAndUpload(); + }); + } catch (e) { + debugPrint('VideoRecordingService: Failed to start recording β€” $e'); + _cleanup(); + } + } + + /// Stop recording and upload the video to Firebase Storage. + static Future stopAndUpload() async { + if (!_isRecording || _controller == null) return; + + _stopTimer?.cancel(); + _stopTimer = null; + + try { + // Stop recording + final videoFile = await _controller!.stopVideoRecording(); + _isRecording = false; + debugPrint('VideoRecordingService: Recording stopped β€” ${videoFile.path}'); + + // Copy to persistent local directory so we can play it back later + final savedPath = await _saveLocally(videoFile.path); + + // Save emergency metadata to SharedPreferences + await _saveEmergencyRecord(savedPath); + + // Upload to Firebase Storage (in background) + _uploadVideo(videoFile.path); + } catch (e) { + debugPrint('VideoRecordingService: Failed to stop recording β€” $e'); + } finally { + _cleanup(); + } + } + + /// Copy recording to app's persistent documents directory. + static Future _saveLocally(String tempPath) async { + try { + final dir = await getApplicationDocumentsDirectory(); + final sosDir = Directory('${dir.path}/sos_recordings'); + if (!await sosDir.exists()) await sosDir.create(recursive: true); + + final now = DateTime.now(); + final fileName = 'SOS_${now.millisecondsSinceEpoch}.mp4'; + final destPath = '${sosDir.path}/$fileName'; + + await File(tempPath).copy(destPath); + debugPrint('VideoRecordingService: Saved locally at $destPath'); + return destPath; + } catch (e) { + debugPrint('VideoRecordingService: Failed to save locally β€” $e'); + return tempPath; // fallback to temp path + } + } + + /// Store emergency metadata in SharedPreferences as JSON. + static Future _saveEmergencyRecord(String filePath) async { + try { + final prefs = await SharedPreferences.getInstance(); + final emergencies = prefs.getStringList('emergencies') ?? []; + + emergencies.add(jsonEncode({ + 'time': DateTime.now().toIso8601String(), + 'filePath': filePath, + 'type': 'video', + })); + + await prefs.setStringList('emergencies', emergencies); + debugPrint('VideoRecordingService: Emergency record saved to prefs'); + } catch (e) { + debugPrint('VideoRecordingService: Failed to save record β€” $e'); + } + } + + /// Upload a video file to Firebase Storage. + static Future _uploadVideo(String filePath) async { + try { + final file = File(filePath); + if (!await file.exists()) { + debugPrint('VideoRecordingService: File does not exist'); + return; + } + + // Generate a unique filename with timestamp + final now = DateTime.now(); + final fileName = + 'SOS_${now.year}${now.month.toString().padLeft(2, '0')}' + '${now.day.toString().padLeft(2, '0')}_' + '${now.hour.toString().padLeft(2, '0')}' + '${now.minute.toString().padLeft(2, '0')}' + '${now.second.toString().padLeft(2, '0')}.mp4'; + + final ref = FirebaseStorage.instance.ref('sos_recordings/$fileName'); + await ref.putFile( + file, + SettableMetadata(contentType: 'video/mp4'), + ); + + debugPrint('VideoRecordingService: Video uploaded as $fileName'); + } catch (e) { + debugPrint('VideoRecordingService: Upload failed β€” $e'); + } + } + + /// Release camera resources. + static void _cleanup() { + _controller?.dispose(); + _controller = null; + _isRecording = false; + } + + /// Check if currently recording. + static bool get isRecording => _isRecording; +} diff --git a/Team-Shivam/SheShield/lib/services/voice_trigger_service.dart b/Team-Shivam/SheShield/lib/services/voice_trigger_service.dart new file mode 100644 index 0000000..54805d2 --- /dev/null +++ b/Team-Shivam/SheShield/lib/services/voice_trigger_service.dart @@ -0,0 +1,163 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart' show debugPrint; +import 'package:speech_to_text/speech_to_text.dart' as stt; + +/// Keywords that trigger the SOS alert when spoken. +const _sosKeywords = [ + 'help', + 'help me', + 'emergency', + 'she shield', + 'sheshield', + 'police', + 'bachao', + 'bachaao', + 'bachaaao', + 'danger', + 'save me', + 'sos', + 'call police', + 'please help', +]; + +/// Service for continuous voice monitoring. +/// +/// Listens for speech in the background while the app is open and invokes +/// [onSosDetected] when any keyword from [_sosKeywords] is recognised. +class VoiceTriggerService { + final stt.SpeechToText _speech = stt.SpeechToText(); + final void Function() onSosDetected; + + bool _isInitialized = false; + bool _sosFired = false; + Timer? _restartTimer; + Timer? _autoResetTimer; + bool _disposed = false; + + VoiceTriggerService({required this.onSosDetected}); + + /// Initialize the speech recogniser. Returns `true` if available. + Future initialize() async { + try { + _isInitialized = await _speech.initialize( + onStatus: _onStatus, + onError: (error) { + debugPrint('VoiceTrigger: Error β€” ${error.errorMsg}'); + _scheduleRestart(); + }, + ); + debugPrint('VoiceTrigger: Initialized = $_isInitialized'); + } catch (e) { + debugPrint('VoiceTrigger: Init failed β€” $e'); + _isInitialized = false; + } + return _isInitialized; + } + + /// Start continuous listening. Safe to call multiple times. + void startListening() { + if (!_isInitialized || _disposed) return; + _sosFired = false; + _stopSpeech(); + _restartTimer?.cancel(); + _restartTimer = Timer(const Duration(milliseconds: 500), _listen); + } + + /// Stop listening and cancel any pending restart timers. + void stopListening() { + _restartTimer?.cancel(); + _restartTimer = null; + _stopSpeech(); + } + + /// Reset the SOS-fired flag and resume listening. + void resetAndRestart() { + _autoResetTimer?.cancel(); + _sosFired = false; + _stopSpeech(); + _restartTimer?.cancel(); + _restartTimer = Timer(const Duration(milliseconds: 500), _listen); + } + + /// Clean up resources. + void dispose() { + _disposed = true; + _autoResetTimer?.cancel(); + stopListening(); + try { _speech.cancel(); } catch (_) {} + } + + // --------------------------------------------------------------------------- + + void _stopSpeech() { + try { + if (_speech.isListening) { + _speech.stop(); + } + } catch (_) {} + } + + void _listen() { + if (_sosFired || !_isInitialized || _disposed) return; + + if (_speech.isListening) { + _stopSpeech(); + _restartTimer?.cancel(); + _restartTimer = Timer(const Duration(milliseconds: 500), _listen); + return; + } + + debugPrint('VoiceTrigger: Starting speech recognition…'); + + try { + _speech.listen( + onResult: (result) { + final words = result.recognizedWords.toLowerCase().trim(); + if (words.isEmpty) return; + + debugPrint('VoiceTrigger: Heard "$words" (final=${result.finalResult})'); + + for (final keyword in _sosKeywords) { + if (words.contains(keyword)) { + debugPrint('VoiceTrigger: 🚨 Keyword matched: "$keyword"'); + _sosFired = true; + stopListening(); + onSosDetected(); + // Auto-reset after 20s so voice can trigger again + _autoResetTimer = Timer(const Duration(seconds: 20), () { + if (!_disposed) resetAndRestart(); + }); + return; + } + } + }, + listenFor: const Duration(seconds: 30), + pauseFor: const Duration(seconds: 5), + listenOptions: stt.SpeechListenOptions( + listenMode: stt.ListenMode.dictation, + cancelOnError: false, + partialResults: true, + autoPunctuation: false, + ), + ); + } catch (e) { + debugPrint('VoiceTrigger: Listen failed β€” $e'); + _scheduleRestart(); + } + } + + void _onStatus(String status) { + debugPrint('VoiceTrigger: Status β†’ $status'); + if (status == 'done' || status == 'notListening') { + _scheduleRestart(); + } + } + + void _scheduleRestart() { + if (_sosFired || _disposed) return; + _restartTimer?.cancel(); + _restartTimer = Timer(const Duration(milliseconds: 500), () { + if (!_sosFired && !_disposed) _listen(); + }); + } +} diff --git a/Team-Shivam/SheShield/pubspec.yaml b/Team-Shivam/SheShield/pubspec.yaml new file mode 100644 index 0000000..d00b39d --- /dev/null +++ b/Team-Shivam/SheShield/pubspec.yaml @@ -0,0 +1,44 @@ +name: sheshield +description: She Shield – Smart Safety Bracelet. A Flutter app for emergency SOS alerts and GPS location sharing. + +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.0.0 + +dependencies: + flutter: + sdk: flutter + + cupertino_icons: ^1.0.6 + firebase_core: ^3.8.1 + firebase_auth: ^5.5.1 + cloud_firestore: ^5.6.2 + geolocator: ^11.0.0 + google_maps_flutter: ^2.6.0 + shared_preferences: ^2.2.2 + share_plus: ^7.2.1 + permission_handler: ^11.3.0 + url_launcher: ^6.2.4 + firebase_messaging: ^15.2.1 + http: ^1.2.0 + flutter_bluetooth_serial_ble: ^0.5.0 + speech_to_text: ^7.0.0 + camera: ^0.11.0+2 + camera_android: ^0.10.10 + firebase_storage: ^12.4.0 + path_provider: ^2.1.2 + connectivity_plus: ^6.1.4 + video_player: ^2.11.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.1 + +flutter: + uses-material-design: true + assets: + - assets/images/ diff --git a/Team-Shivam/SheShield/screenshots & video/add emergenc contacts .jpeg b/Team-Shivam/SheShield/screenshots & video/add emergenc contacts .jpeg new file mode 100644 index 0000000..08da408 Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/add emergenc contacts .jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/band not connected .jpeg b/Team-Shivam/SheShield/screenshots & video/band not connected .jpeg new file mode 100644 index 0000000..aa18702 Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/band not connected .jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/bluetooth connected .jpeg b/Team-Shivam/SheShield/screenshots & video/bluetooth connected .jpeg new file mode 100644 index 0000000..b2ea33e Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/bluetooth connected .jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/homepage 1 .jpeg b/Team-Shivam/SheShield/screenshots & video/homepage 1 .jpeg new file mode 100644 index 0000000..a336e05 Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/homepage 1 .jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/homepage.jpeg b/Team-Shivam/SheShield/screenshots & video/homepage.jpeg new file mode 100644 index 0000000..cb18b01 Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/homepage.jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/live location.jpeg b/Team-Shivam/SheShield/screenshots & video/live location.jpeg new file mode 100644 index 0000000..58c996e Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/live location.jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/nearby police stations .jpeg b/Team-Shivam/SheShield/screenshots & video/nearby police stations .jpeg new file mode 100644 index 0000000..627b57f Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/nearby police stations .jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/past emergencies .jpeg b/Team-Shivam/SheShield/screenshots & video/past emergencies .jpeg new file mode 100644 index 0000000..9d73812 Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/past emergencies .jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/profile.jpeg b/Team-Shivam/SheShield/screenshots & video/profile.jpeg new file mode 100644 index 0000000..89f77d3 Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/profile.jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/share location.jpeg b/Team-Shivam/SheShield/screenshots & video/share location.jpeg new file mode 100644 index 0000000..a83bd42 Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/share location.jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/sign in .jpeg b/Team-Shivam/SheShield/screenshots & video/sign in .jpeg new file mode 100644 index 0000000..4272ed8 Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/sign in .jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/signup.jpeg b/Team-Shivam/SheShield/screenshots & video/signup.jpeg new file mode 100644 index 0000000..21b834b Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/signup.jpeg differ diff --git a/Team-Shivam/SheShield/screenshots & video/sos triggered.jpeg b/Team-Shivam/SheShield/screenshots & video/sos triggered.jpeg new file mode 100644 index 0000000..b4cb6f8 Binary files /dev/null and b/Team-Shivam/SheShield/screenshots & video/sos triggered.jpeg differ diff --git a/Team-Shivam/SheShield/test/widget_test.dart b/Team-Shivam/SheShield/test/widget_test.dart new file mode 100644 index 0000000..e5b5f60 --- /dev/null +++ b/Team-Shivam/SheShield/test/widget_test.dart @@ -0,0 +1,9 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sheshield/main.dart'; + +void main() { + testWidgets('App smoke test', (WidgetTester tester) async { + await tester.pumpWidget(const SheShieldApp()); + expect(find.text('SOS'), findsOneWidget); + }); +}