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