[
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Nizar Mahmoud\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "PRIVACY.md",
    "content": "# Privacy Policy\n\n**Last Updated:** November 1, 2024\n\n## Introduction\n\n**Igatha** is committed to protecting your privacy. This Privacy Policy outlines how the app functions and the measures we take to ensure your information remains secure.\n\n## How Igatha Works\n\n### 1. Sensor Data\n- **Accelerometer Readings:** Used locally for detecting sudden movements to identify potential emergencies.\n- **Gyroscope Data:** Utilized locally to determine device orientation, aiding in emergency detection.\n- **Barometer Readings:** Monitors pressure changes locally to assist in disaster detection.\n- **Data Handling:** All sensor data is processed locally on your device and immediately discarded. No data is transmitted off your device.\n\n### 2. Bluetooth\n- **Bluetooth Low Energy (BLE) Signals:** Used locally for broadcasting SOS beacons and scanning for nearby signals.\n- **Signal Strength:** Assessed locally to estimate the distance of detected signals.\n- **Temporary Device Identifiers:** Uses the first 8 characters of UUIDs for temporary identification purposes.\n- **Data Handling:** No persistent storage of detected devices. All Bluetooth interactions are handled locally without data retention or transmission.\n\n### 3. Location Services\n- **Background Location Permission:** Required to enable sensor monitoring for automated disaster detection on iOS.\n- **Bluetooth Scanning Permission:** Required to enable bluetooth scanning for nearby devices on Android 11  and lower.\n- **Data Handling:** We do **not** transmit or store GPS coordinates or perform location tracking. Location permissions are solely used to facilitate sensor-based emergency detection locally on your device.\n\n### 4. Foreground Services\n- **Background Permission:** Required to enable sensor monitoring for automated disaster detection and signaling SOS in the background on Android.\n\n## Data Privacy\n\n- **Local Processing:** All data processing occurs on your device. No data is transmitted over the internet.\n- **No Data Collection:** Data is not transmitted off your device or persisted between app sessions. Once processed, it is immediately discarded.\n- **No Third-Party Integration:** Igatha does not use any analytics tools, tracking mechanisms, or third-party SDKs.\n- **No Cloud Services:** There is no cloud integration or external servers involved.\n\n## Permissions Used\n\n- **Bluetooth:** Enables local SOS beacon transmission and detection.\n- **Location:** Required for background sensor operations to facilitate automated emergency detection on iOS. Required for bluetooth scanning on Android 11 and lower.\n- **Motion/Sensors:** Access to device motion and sensors for local disaster detection.\n- **Foreground Services:** Required to enable sensor monitoring for automated disaster detection and signaling SOS in the background on Android.\n\n## Open Source Commitment\n\nIgatha is open-sourced under the [GNU General Public License v3.0](https://github.com/nizarmah/igatha/blob/main/LICENSE). You can review, modify, and contribute to the source code at [github.com/nizarmah/igatha](https://github.com/nizarmah/igatha).\n\n## Contact Us\n\nIf you have any questions or concerns about this Privacy Policy, please contact us at [nizarmah@hotmail.com](mailto:nizarmah@hotmail.com).\n"
  },
  {
    "path": "README.md",
    "content": "# Igatha\n\nIgatha is an open-source SOS signaling and recovery app designed for war zones and disaster areas, enabling offline emergency communication when traditional networks fail.\n\n## Status\n\n- **iOS**: [v1.0](https://apps.apple.com/us/app/igatha/id6737691452)\n- **Android**: [v1.0](https://play.google.com/store/apps/details?id=com.nizarmah.igatha)\n\n## Quickstart\n\n1. Install the app using the links above.\n1. Open the app and grant the necessary permissions.\n\n## How to use Igatha\n\n### Sending SOS signals (distress mode)\n\n#### Manual signaling\n\n1. Open Igatha.\n1. Ensure bluetooth is enabled.\n1. Tap \"_Send SOS_\".\n\n#### Automatic signaling\n\n1. Open Igatha.\n1. Tap the gear icon (top right).\n1. Enable \"_Disaster Detection_\".\n\nWith disaster detection, Igatha will now run in the background, monitoring your device's sensors.\nWhen a potential disaster is detected, you'll receive an \"_Are you okay?_\" notification:\n* If you respond with \"_Need help_\" or don't respond in 2 minutes, it will automatically broadcast an SOS.\n* If you respond with \"_I'm okay_\", it will ignore the event.\n\n### Helping others (recovery mode)\n\nIf you're safe and want to help others:\n\n1. Open Igatha.\n1. Ensure bluetooth is enabled (on Android 11 or lower, also enable Location).\n1. Check \"_People seeking help_\".\n1. Move towards locations where displayed distances decrease.\n1. Listen carefully for audible sirens.\n\n## How Igatha works\n\n### Bluetooth low energy (BLE)\n\nIgatha uses Bluetooth Low Energy (BLE) to:\n1. Broadcast SOS signals.\n1. Scan for nearby SOS broadcasts.\n1. Estimate approximate distance to the signal source based on signal strength.\n\nNo internet or GPS is required, preventing signal jamming or manipulation.\n\n### SOS signal composition\n\nThe SOS signal combines:\n1. BLE advertisement: broadcasts a pseudonymized identifier.\n1. Audible siren: generated via device speakers to help responders locate you.\n\nResponders can toggle additional signals, like flashlight or vibration, remotely. (planned feature)\n\n### Disaster detection sensors\n\nIgatha detects disasters using device sensors:\n1. Accelerometer: measures sudden motion changes.\n1. Gyroscope: detect orientation and rotation shifts.\n1. Barometer (if available): detects atmospheric pressure changes, reducing false positives.\n\nDisaster detection triggers when multiple sensors simultaneously detect abrupt changes.\n\nLocation permissions are required for \"_Disaster Detection_\".\n\n## Battery usage\n\nIgatha minimizes battery use by leveraging BLE and optimized sensor monitoring.\n\nThe app can continuously broadcast for extended periods during emergencies.\n\n## Limitations\n\n### Early stage\n\n* This is a Minimum Viable Product (MVP) with significant room for improvement\n* Testing has been limited to controlled environments\n* While not guaranteed to work in all scenarios, it provides a potential lifeline where no alternatives exist\n\n### Signal range\n\n* BLE range: typically 10-30 meters indoors, further outdoors, limited by rubble and building materials.\n* Optional extensions: Third-party BLE receivers can extend range significantly.\n\n## Why open source?\n\nIgatha is open-sourced for:\n\n1. **Transparency**: In crisis situations, people need to trust the tools they use. Open source allows anyone to verify the app's security and privacy measures.\n\n2. **Accessibility**: Making the code freely available ensures the technology can be used, studied, and adapted by anyone who needs it.\n\n3. **Community Impact**: War and disaster response tools should be community resources, not commercial products. Open sourcing enables collaborative improvement and adaptation for different crisis scenarios.\n\n## Contributing\n\nContributions are vital for improving Igatha:\n* Testing and bug reports\n* Documentation\n* Translations\n* Feature enhancements\n* Code optimization\n* Security reviews\n* Distribution\n\nTo contribute, open an issue or submit a pull request.\n\n## Privacy & security\n\n* Completely offline; no data collection or internet connectivity.\n* Uses pseudonymized identifiers for privacy.\n\n## Contact\n\nFor questions, suggestions, or feedback, please open an issue in the repository.\n"
  },
  {
    "path": "android/.gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n"
  },
  {
    "path": "android/.gitkeep",
    "content": ""
  },
  {
    "path": "android/.idea/.gitignore",
    "content": "# Default ignored files\n/shelf/\n/workspace.xml\n"
  },
  {
    "path": "android/.idea/.name",
    "content": "Igatha"
  },
  {
    "path": "android/.idea/AndroidProjectSystem.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"AndroidProjectSystem\">\n    <option name=\"providerId\" value=\"com.android.tools.idea.GradleProjectSystem\" />\n  </component>\n</project>"
  },
  {
    "path": "android/.idea/compiler.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"CompilerConfiguration\">\n    <bytecodeTargetLevel target=\"21\" />\n  </component>\n</project>"
  },
  {
    "path": "android/.idea/deploymentTargetSelector.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"deploymentTargetSelector\">\n    <selectionStates>\n      <SelectionState runConfigName=\"app\">\n        <option name=\"selectionMode\" value=\"DROPDOWN\" />\n      </SelectionState>\n    </selectionStates>\n  </component>\n</project>"
  },
  {
    "path": "android/.idea/deviceManager.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"DeviceTable\">\n    <option name=\"columnSorters\">\n      <list>\n        <ColumnSorterState>\n          <option name=\"column\" value=\"Name\" />\n          <option name=\"order\" value=\"ASCENDING\" />\n        </ColumnSorterState>\n      </list>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": "android/.idea/gradle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"GradleMigrationSettings\" migrationVersion=\"1\" />\n  <component name=\"GradleSettings\">\n    <option name=\"linkedExternalProjectsSettings\">\n      <GradleProjectSettings>\n        <option name=\"testRunner\" value=\"CHOOSE_PER_TEST\" />\n        <option name=\"externalProjectPath\" value=\"$PROJECT_DIR$\" />\n        <option name=\"gradleJvm\" value=\"#GRADLE_LOCAL_JAVA_HOME\" />\n        <option name=\"modules\">\n          <set>\n            <option value=\"$PROJECT_DIR$\" />\n            <option value=\"$PROJECT_DIR$/app\" />\n          </set>\n        </option>\n        <option name=\"resolveExternalAnnotations\" value=\"false\" />\n      </GradleProjectSettings>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": "android/.idea/inspectionProfiles/Project_Default.xml",
    "content": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project Default\" />\n    <inspection_tool class=\"ComposePreviewDimensionRespectsLimit\" enabled=\"true\" level=\"WARNING\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"ComposePreviewMustBeTopLevelFunction\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"ComposePreviewNeedsComposableAnnotation\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"ComposePreviewNotSupportedInUnitTestFiles\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"GlancePreviewDimensionRespectsLimit\" enabled=\"true\" level=\"WARNING\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"GlancePreviewMustBeTopLevelFunction\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"GlancePreviewNeedsComposableAnnotation\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"GlancePreviewNotSupportedInUnitTestFiles\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewAnnotationInFunctionWithParameters\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewApiLevelMustBeValid\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewDeviceShouldUseNewSpec\" enabled=\"true\" level=\"WEAK WARNING\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewFontScaleMustBeGreaterThanZero\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewMultipleParameterProviders\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewParameterProviderOnFirstParameter\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewPickerAnnotation\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n      <option name=\"previewFile\" value=\"true\" />\n    </inspection_tool>\n  </profile>\n</component>"
  },
  {
    "path": "android/.idea/kotlinc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"KotlinJpsPluginSettings\">\n    <option name=\"version\" value=\"2.0.0\" />\n  </component>\n</project>"
  },
  {
    "path": "android/.idea/migrations.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectMigrations\">\n    <option name=\"MigrateToGradleLocalJavaHome\">\n      <set>\n        <option value=\"$PROJECT_DIR$\" />\n      </set>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": "android/.idea/misc.xml",
    "content": "<project version=\"4\">\n  <component name=\"ExternalStorageConfigurationManager\" enabled=\"true\" />\n  <component name=\"ProjectRootManager\" version=\"2\" languageLevel=\"JDK_21\" default=\"true\" project-jdk-name=\"jbr-21\" project-jdk-type=\"JavaSDK\">\n    <output url=\"file://$PROJECT_DIR$/build/classes\" />\n  </component>\n  <component name=\"ProjectType\">\n    <option name=\"id\" value=\"Android\" />\n  </component>\n</project>"
  },
  {
    "path": "android/.idea/runConfigurations.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"RunConfigurationProducerService\">\n    <option name=\"ignoredProducers\">\n      <set>\n        <option value=\"com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.AllInPackageConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.PatternConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.TestInClassConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.UniqueIdConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer\" />\n        <option value=\"org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer\" />\n        <option value=\"org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer\" />\n      </set>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": "android/.idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$/..\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "android/app/.gitignore",
    "content": "/build"
  },
  {
    "path": "android/app/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.android.application)\n    alias(libs.plugins.kotlin.android)\n    alias(libs.plugins.kotlin.compose)\n\n    id(\"kotlin-parcelize\")\n}\n\nandroid {\n    namespace = \"com.nizarmah.igatha\"\n    compileSdk = 35\n\n    defaultConfig {\n        applicationId = \"com.nizarmah.igatha\"\n        minSdk = 26\n        targetSdk = 35\n        versionCode = 2\n        versionName = \"1.1.0\"\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_11\n        targetCompatibility = JavaVersion.VERSION_11\n    }\n    kotlinOptions {\n        jvmTarget = \"11\"\n    }\n    buildFeatures {\n        compose = true\n    }\n\n    dependenciesInfo {\n        // Exclude dependency block from APKs for open source compliance.\n        // @see https://android.izzysoft.de/articles/named/iod-scan-apkchecks#blobs\n        includeInApk = false\n        // Include dependency block in AAB for playstore compliance.\n        // @see https://developer.android.com/build/dependencies#dependency-info-play\n        includeInBundle = true\n    }\n}\n\ndependencies {\n\n    implementation(libs.androidx.core.ktx)\n    implementation(libs.androidx.lifecycle.runtime.ktx)\n    implementation(libs.androidx.activity.compose)\n    implementation(platform(libs.androidx.compose.bom))\n    implementation(libs.androidx.ui)\n    implementation(libs.androidx.ui.graphics)\n    implementation(libs.androidx.ui.tooling.preview)\n    implementation(libs.androidx.material3)\n\n    implementation(libs.androidx.navigation.compose)\n\n    implementation(libs.gson)\n\n    implementation(libs.androidx.work.runtime.ktx)\n\n    testImplementation(libs.junit)\n    androidTestImplementation(libs.androidx.junit)\n    androidTestImplementation(libs.androidx.espresso.core)\n    androidTestImplementation(platform(libs.androidx.compose.bom))\n    androidTestImplementation(libs.androidx.ui.test.junit4)\n    debugImplementation(libs.androidx.ui.tooling)\n    debugImplementation(libs.androidx.ui.test.manifest)\n}\n"
  },
  {
    "path": "android/app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "android/app/src/androidTest/java/com/nizarmah/igatha/ExampleInstrumentedTest.kt",
    "content": "package com.nizarmah.igatha\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.nizarmah.igatha\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <!-- FeedbackFormViewModel - Required for Google Forms submission -->\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <!-- ProximityScanner | SOSBeacon -->\n    <uses-permission android:name=\"android.permission.BLUETOOTH\" />\n    <uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" />\n\n    <!-- ProximityScanner -->\n    <uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\" />\n    <uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" />\n    <!-- See: https://source.android.com/docs/core/connect/bluetooth/ble#location-scanning -->\n    <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"\n        android:maxSdkVersion=\"30\" />\n\n    <!-- SOSBeacon -->\n    <uses-permission android:name=\"android.permission.BLUETOOTH_ADVERTISE\" />\n    <!-- SirenPlayer -->\n    <uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />\n\n    <!-- SOSService | DisasterDetectionService -->\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_HEALTH\" />\n    <uses-permission android:name=\"android.permission.HIGH_SAMPLING_RATE_SENSORS\" />\n\n    <!-- See: https://developer.android.com/develop/ui/views/notifications/notification-permission -->\n    <!-- See: com.nizarmah.igatha.util.PermissionsHelper -->\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\"\n        android:minSdkVersion=\"33\" />\n\n    <application\n        android:name=\".IgathaApp\"\n        android:allowBackup=\"true\"\n        android:dataExtractionRules=\"@xml/data_extraction_rules\"\n        android:fullBackupContent=\"@xml/backup_rules\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.Igatha\"\n        tools:targetApi=\"31\">\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:label=\"@string/app_name\"\n            android:theme=\"@style/Theme.Igatha\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <!-- Deep link handling -->\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n                <data android:scheme=\"igatha\" />\n            </intent-filter>\n        </activity>\n\n        <service\n            android:name=\".service.DisasterDetectionService\"\n            android:enabled=\"true\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"health\"\n            android:stopWithTask=\"false\" />\n\n        <service\n            android:name=\".service.SOSService\"\n            android:enabled=\"true\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"health\"\n            android:stopWithTask=\"false\" />\n    </application>\n\n</manifest>"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/Constants.kt",
    "content": "package com.nizarmah.igatha\n\nimport android.os.ParcelUuid\n\nobject Constants {\n    val SOS_BEACON_SERVICE_UUID: ParcelUuid = ParcelUuid.fromString(\"00001802-0000-1000-8000-00805F9B34FB\") // 1802\n\n    // update interval for sensor readings in microseconds\n    const val SENSOR_UPDATE_INTERVAL: Int = 100_000\n    // threshold for sudden changes in linear acceleration\n    // 3.0 g ~= dropping your phone on a hard surface\n    const val SENSOR_ACCELERATION_THRESHOLD: Double = 3.0\n    // threshold for sudden changes in rotation\n    // 6.0 r/s ~= almost a full rotation in 1 second\n    const val SENSOR_ROTATION_THRESHOLD: Double = 6.0\n    // threshold for sudden changes in atmospheric pressure\n    // 0.1 kPa ~= altitude change of approx. 8 to 12 meters\n    const val SENSOR_PRESSURE_THRESHOLD: Double = 0.1\n\n    // key for shared preferences collection\n    const val SHARED_PREFERENCES_KEY: String = \"com.nizarmah.igatha.preferences\"\n    // key for disaster detector enabled setting in preferences\n    const val DISASTER_DETECTION_ENABLED_KEY: String = \"disasterDetectionEnabled\"\n    // time window for temporally correlating sensor readings\n    // if all thresholds exceed in 1.5s then we have an incident\n    const val DISASTER_TEMPORAL_CORRELATION_TIME_WINDOW: Double = 1.5\n    // grace period (seconds) before an incident response is triggered\n    const val DISASTER_RESPONSE_GRACE_PERIOD: Double = 120.0\n\n    // percentage of how much a new value should affect the old value\n    const val RSSI_EXPONENTIAL_MOVING_AVERAGE_SMOOTHING_FACTOR: Double = 0.18\n\n    const val DISASTER_MONITORING_NOTIFICATION_ID: Int = 1\n    const val DISASTER_MONITORING_NOTIFICATION_KEY: String = \"DISASTER_MONITORING\"\n    const val DISASTER_RESPONSE_NOTIFICATION_ID: Int = 2\n    const val DISASTER_RESPONSE_NOTIFICATION_KEY: String = \"DISASTER_RESPONSE\"\n    const val DISTRESS_ACTIVE_NOTIFICATION_ID: Int = 3\n    const val DISTRESS_ACTIVE_NOTIFICATION_KEY: String = \"DISTRESS_ACTIVE\"\n\n    const val FEEDBACK_NOTIFICATION_ID: Int = 4\n    const val FEEDBACK_NOTIFICATION_KEY: String = \"FEEDBACK_REQUEST\"\n\n    const val ACTION_IGNORE_ALERT: String = \"com.nizarmah.igatha.actions.IGNORE_ALERT\"\n    const val ACTION_START_SOS: String = \"com.nizarmah.igatha.actions.START_SOS\"\n    const val ACTION_STOP_SOS: String = \"com.nizarmah.igatha.actions.STOP_SOS\"\n\n    // Deep links\n    object DeepLink {\n        const val SCHEME = \"igatha\"\n\n        object Settings {\n            const val VALUE = \"settings\"\n        }\n    }\n\n    // Notification constants\n    object Notifications {\n        object Feedback {\n            const val ID = \"feedbackRequest\"\n            val LINK = DeepLink.Settings\n\n            // Delay before notification is shown (3 days in seconds)\n            const val TRIGGER_DELAY: Long = 3 * 24 * 60 * 60\n\n            // Key for timestamp of when feedback request was scheduled\n            const val TIMESTAMP_KEY = \"feedbackScheduledTimestamp\"\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/IgathaApp.kt",
    "content": "package com.nizarmah.igatha\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport com.nizarmah.igatha.util.PermissionsManager\nimport com.nizarmah.igatha.util.SettingsManager\nimport androidx.work.OneTimeWorkRequestBuilder\nimport androidx.work.WorkManager\nimport androidx.work.ExistingWorkPolicy\nimport java.util.concurrent.TimeUnit\nimport com.nizarmah.igatha.service.FeedbackWorker\nimport androidx.core.content.edit\n\nclass IgathaApp : Application() {\n    override fun onCreate() {\n        super.onCreate()\n\n        SettingsManager.init(this)\n        PermissionsManager.init(this)\n\n        var sharedPrefs = getSharedPreferences(\n            Constants.SHARED_PREFERENCES_KEY,\n            MODE_PRIVATE\n        )\n\n        var workManager = WorkManager.getInstance(this)\n\n        scheduleFeedbackNotification(\n            PermissionsManager.notificationsPermitted.value,\n            sharedPrefs,\n            workManager\n        )\n    }\n\n    // scheduleFeedbackNotification is used to schedule the feedback notification.\n    private fun scheduleFeedbackNotification(\n        notificationsPermitted: Boolean,\n        sharedPrefs: SharedPreferences,\n        workManager: WorkManager\n    ) {\n        if (\n            // Check if notifications are not permitted\n            !notificationsPermitted ||\n            // Check if feedback notification was already scheduled\n            sharedPrefs.contains(Constants.Notifications.Feedback.TIMESTAMP_KEY)\n        ) {\n            return\n        }\n\n        // Schedule feedback notification once using WorkManager\n        val feedbackWorkRequest = OneTimeWorkRequestBuilder<FeedbackWorker>()\n            .setInitialDelay(\n                Constants.Notifications.Feedback.TRIGGER_DELAY,\n                TimeUnit.SECONDS\n            )\n            .build()\n\n        // Enqueue the work request\n        workManager\n            .enqueueUniqueWork(\n                Constants.Notifications.Feedback.ID,\n                ExistingWorkPolicy.KEEP,\n                feedbackWorkRequest\n            )\n\n        // Mark feedback notification as scheduled\n        sharedPrefs.edit {\n            putLong(\n                Constants.Notifications.Feedback.TIMESTAMP_KEY,\n                System.currentTimeMillis()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/MainActivity.kt",
    "content": "package com.nizarmah.igatha\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport com.nizarmah.igatha.ui.component.PermissionHandler\nimport com.nizarmah.igatha.ui.screen.ContentScreen\nimport com.nizarmah.igatha.ui.theme.IgathaTheme\nimport com.nizarmah.igatha.util.PermissionsManager\n\nclass MainActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enableEdgeToEdge()\n        setContent {\n            IgathaTheme(\n                // Disable dynamic wallpaper-based theming\n                // so our static red primary is used instead\n                dynamicColor = false\n            ) {\n                MainScreen()\n            }\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        PermissionsManager.refreshPermissions(this)\n    }\n}\n\n@Composable\nfun MainScreen() {\n    Scaffold(\n        bottomBar = {\n            PermissionHandler(\n                modifier = Modifier.fillMaxWidth()\n            )\n        },\n        content = { paddingValues ->\n            ContentScreen(\n                modifier = Modifier\n                    .padding(paddingValues)\n                    .fillMaxSize()\n            )\n        }\n    )\n}\n\n@Preview(showBackground = true)\n@Composable\nfun MainActivityPreview() {\n    MainActivity()\n}"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/model/Device.kt",
    "content": "package com.nizarmah.igatha.model\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\nimport java.util.*\nimport kotlin.math.pow\nimport kotlin.math.roundToInt\nimport com.nizarmah.igatha.Constants\n\nfun UUID.shortName(): String {\n    return this.toString().substring(0, 8).uppercase()\n}\n\n@Parcelize\ndata class Device(\n    val id: String,\n    val shortName: String,\n    var rssi: Double,\n    var lastSeen: Date = Date()\n) : Parcelable {\n    constructor(\n        id: UUID,\n        rssi: Double,\n        lastSeen: Date = Date()\n    ) : this(\n        id.toString().uppercase(),\n        id.shortName(),\n        rssi,\n        lastSeen\n    )\n\n    fun update(rssi: Double, lastSeen: Date = Date()) {\n        val oldRSSI = this.rssi\n        val newRSSI = rssi\n\n        // smoothing factor\n        val alpha = Constants.RSSI_EXPONENTIAL_MOVING_AVERAGE_SMOOTHING_FACTOR\n\n        // smoothen the RSSI with exponential moving average\n        val smoothedRSSI = alpha * newRSSI + (1 - alpha) * oldRSSI\n\n        this.rssi = smoothedRSSI\n        this.lastSeen = lastSeen\n    }\n\n    fun estimateDistance(pathLossExponent: PathLossExponent = PathLossExponent.URBAN): Double {\n        // 1 meter ~= -59.0 RSSI\n        val txPower = -59.0\n\n        // path-loss exponent\n        val n: Double = pathLossExponent.value\n\n        val distance = 10.0.pow((txPower - rssi) / (10.0 * n))\n\n        // round for simplicity\n        return (distance * 1000.0).roundToInt() / 1000.0\n    }\n}\n\nenum class PathLossExponent(val value: Double) {\n    // path-loss exponent (n)\n\n    // free open spaces\n    // n = 2.0\n    FREE_SPACE(2.0),\n\n    // indoor environments\n    // n = 3.0\n    INDOOR(3.0),\n\n    // dense urban environments\n    // n = 4.0\n    URBAN(4.0)\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/sensor/AcceleratorSensor.kt",
    "content": "package com.nizarmah.igatha.sensor\n\nimport android.content.Context\nimport android.hardware.Sensor\nimport android.hardware.SensorEvent\nimport android.hardware.SensorEventListener\nimport android.hardware.SensorManager\nimport android.util.Log\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlin.math.sqrt\nimport com.nizarmah.igatha.sensor.Sensor as InternalSensor\n\nclass AcceleratorSensor(\n    context: Context,\n    override val threshold: Double, // Threshold in g's\n    private val updateInterval: Int // in microseconds\n) : InternalSensor, SensorEventListener {\n    private val sensorManager: SensorManager =\n        context.getSystemService(Context.SENSOR_SERVICE) as SensorManager\n\n    override val sensor: Sensor? =\n        sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)\n\n    override val sensorType: SensorType = SensorType.ACCELEROMETER\n\n    override val isAvailable: Boolean\n        get() = sensor != null\n\n    private val _events = MutableSharedFlow<SensorCapturedEvent>(\n        replay = 0,\n        extraBufferCapacity = 1,\n        onBufferOverflow = BufferOverflow.DROP_OLDEST\n    )\n    override val events: SharedFlow<SensorCapturedEvent> = _events.asSharedFlow()\n\n    override fun startUpdates() {\n        if (!isAvailable) {\n            Log.w(TAG, \"Accelerometer sensor not available\")\n            return\n        }\n\n        sensor?.let { accelerometer ->\n            sensorManager.registerListener(\n                this,\n                accelerometer,\n                updateInterval\n            )\n            Log.d(TAG, \"AcceleratorSensor: started updates\")\n        }\n    }\n\n    override fun stopUpdates() {\n        sensorManager.unregisterListener(this)\n        Log.d(TAG, \"AcceleratorSensor: stopped updates\")\n    }\n\n    override fun onSensorChanged(event: SensorEvent) {\n        if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) return\n\n        val x = event.values[0]\n        val y = event.values[1]\n        val z = event.values[2]\n\n        // Normalize acceleration to g's\n        val totalAcceleration = sqrt(x * x + y * y + z * z) / SensorManager.GRAVITY_EARTH\n\n        if (totalAcceleration > threshold) {\n            val sensorEvent = SensorCapturedEvent(\n                sensorType = sensorType,\n                eventTime = event.timestamp\n            )\n\n            _events.tryEmit(sensorEvent)\n        }\n    }\n\n    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {\n        // Not used in this implementation\n    }\n\n    companion object {\n        private const val TAG = \"AcceleratorSensor\"\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/sensor/BarometerSensor.kt",
    "content": "package com.nizarmah.igatha.sensor\n\nimport android.content.Context\nimport android.hardware.Sensor\nimport android.hardware.SensorEvent\nimport android.hardware.SensorEventListener\nimport android.hardware.SensorManager\nimport android.util.Log\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlin.math.abs\nimport com.nizarmah.igatha.sensor.Sensor as InternalSensor\n\nclass BarometerSensor(\n    context: Context,\n    override val threshold: Double,\n    private val updateInterval: Int // in microseconds\n) : InternalSensor, SensorEventListener {\n    private val sensorManager: SensorManager =\n        context.getSystemService(Context.SENSOR_SERVICE) as SensorManager\n\n    override val sensor: Sensor? =\n        sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE)\n\n    override val sensorType: SensorType = SensorType.BAROMETER\n\n    override val isAvailable: Boolean\n        get() = sensor != null\n\n    private var initialPressure: Float? = null\n\n    private val _events = MutableSharedFlow<SensorCapturedEvent>(\n        replay = 0,\n        extraBufferCapacity = 1,\n        onBufferOverflow = BufferOverflow.DROP_OLDEST\n    )\n    override val events: SharedFlow<SensorCapturedEvent> = _events.asSharedFlow()\n\n    override fun startUpdates() {\n        if (!isAvailable) {\n            Log.w(TAG, \"Barometer sensor not available\")\n            return\n        }\n\n        initialPressure = null\n\n        sensor?.let { barometer ->\n            sensorManager.registerListener(\n                this,\n                barometer,\n                updateInterval\n            )\n            Log.d(TAG, \"BarometerSensor: started updates\")\n        }\n    }\n\n    override fun stopUpdates() {\n        sensorManager.unregisterListener(this)\n        initialPressure = null\n        Log.d(TAG, \"BarometerSensor: stopped updates\")\n    }\n\n    override fun onSensorChanged(event: SensorEvent) {\n        if (event.sensor.type != Sensor.TYPE_PRESSURE) return\n\n        val pressure = event.values[0] // pressure in hPa (millibar)\n\n        if (initialPressure == null) {\n            initialPressure = pressure\n            return\n        }\n\n        val pressureChange = abs(pressure - (initialPressure ?: return))\n\n        if (pressureChange > threshold) {\n            val sensorEvent = SensorCapturedEvent(\n                sensorType = sensorType,\n                eventTime = event.timestamp\n            )\n            _events.tryEmit(sensorEvent)\n        }\n    }\n\n    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {\n        // Not used in this implementation\n    }\n\n    companion object {\n        private const val TAG = \"BarometerSensor\"\n    }\n}"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/sensor/GyroscopeSensor.kt",
    "content": "package com.nizarmah.igatha.sensor\n\nimport android.content.Context\nimport android.hardware.Sensor\nimport android.hardware.SensorEvent\nimport android.hardware.SensorEventListener\nimport android.hardware.SensorManager\nimport android.util.Log\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlin.math.sqrt\nimport com.nizarmah.igatha.sensor.Sensor as InternalSensor\n\nclass GyroscopeSensor(\n    context: Context,\n    override val threshold: Double, // Threshold in rad/s\n    private val updateInterval: Int // in microseconds\n) : InternalSensor, SensorEventListener {\n    private val sensorManager: SensorManager =\n        context.getSystemService(Context.SENSOR_SERVICE) as SensorManager\n\n    override val sensor: Sensor? =\n        sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)\n\n    override val sensorType: SensorType = SensorType.GYROSCOPE\n\n    override val isAvailable: Boolean\n        get() = sensor != null\n\n    private val _events = MutableSharedFlow<SensorCapturedEvent>(\n        replay = 0,\n        extraBufferCapacity = 1,\n        onBufferOverflow = BufferOverflow.DROP_OLDEST\n    )\n    override val events: SharedFlow<SensorCapturedEvent> = _events.asSharedFlow()\n\n    private var filteredRotationRate = 0.0\n    private val alpha = 0.8 // Low-pass filter coefficient\n\n    override fun startUpdates() {\n        if (!isAvailable) {\n            Log.w(TAG, \"Gyroscope sensor not available\")\n            return\n        }\n\n        sensor?.let { gyroscope ->\n            sensorManager.registerListener(\n                this,\n                gyroscope,\n                updateInterval\n            )\n            Log.d(TAG, \"GyroscopeSensor: started updates\")\n        }\n    }\n\n    override fun stopUpdates() {\n        sensorManager.unregisterListener(this)\n        Log.d(TAG, \"GyroscopeSensor: stopped updates\")\n    }\n\n    override fun onSensorChanged(event: SensorEvent) {\n        if (event.sensor.type != Sensor.TYPE_GYROSCOPE) return\n\n        val x = event.values[0]\n        val y = event.values[1]\n        val z = event.values[2]\n\n        val rawRotationRate = sqrt(x * x + y * y + z * z)\n\n        // Apply low-pass filter\n        filteredRotationRate = alpha * filteredRotationRate + (1 - alpha) * rawRotationRate\n\n        if (filteredRotationRate > threshold) {\n            val sensorEvent = SensorCapturedEvent(\n                sensorType = sensorType,\n                eventTime = event.timestamp\n            )\n            _events.tryEmit(sensorEvent)\n        }\n    }\n\n    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {\n        // Not used in this implementation\n    }\n\n    companion object {\n        private const val TAG = \"GyroscopeSensor\"\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/sensor/SensorType.kt",
    "content": "package com.nizarmah.igatha.sensor\n\nimport android.hardware.Sensor\nimport kotlinx.coroutines.flow.SharedFlow\n\nenum class SensorType {\n    ACCELEROMETER,\n    GYROSCOPE,\n    BAROMETER\n}\n\ninterface AnySensor {\n    val isAvailable: Boolean\n\n    fun startUpdates()\n    fun stopUpdates()\n}\n\ninterface Sensor : AnySensor {\n    val sensor: Sensor?\n    val threshold: Double\n    val sensorType: SensorType\n\n    // A shared flow to emit events when the threshold is exceeded\n    val events: SharedFlow<SensorCapturedEvent>\n}\n\ndata class SensorCapturedEvent(\n    val sensorType: SensorType,\n    val eventTime: Long\n)\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/service/DisasterDetectionService.kt",
    "content": "package com.nizarmah.igatha.service\n\nimport android.app.*\nimport android.content.Intent\nimport android.os.IBinder\nimport androidx.core.app.NotificationCompat\nimport com.nizarmah.igatha.Constants\nimport com.nizarmah.igatha.R\nimport kotlinx.coroutines.*\n\nclass DisasterDetectionService : Service() {\n\n    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())\n\n    private lateinit var emergencyManager: EmergencyManager\n\n    private var confirmationJob: Job? = null\n\n    override fun onCreate() {\n        super.onCreate()\n\n        // Initialize inside onCreate because context is not available earlier\n        emergencyManager = EmergencyManager.getInstance(applicationContext)\n        val started = emergencyManager.startDetector()\n\n        // If the detector did not start, stop the service\n        if (!started) {\n            stopSelf()\n        }\n\n        // Start the service in the foreground with a low-priority notification\n        val notification = createNotification()\n        startForeground(Constants.DISASTER_MONITORING_NOTIFICATION_ID, notification)\n\n        // Observe disaster detection events\n        scope.launch {\n            DisasterEventBus.disasterDetectedFlow.collect {\n                onDisasterDetected()\n            }\n        }\n    }\n\n    private fun onDisasterDetected() {\n        // Show notification to the user\n        showDisasterDetectedNotification()\n\n        // Start confirmation timer\n        startConfirmationTimer()\n    }\n\n    private fun showDisasterDetectedNotification() {\n        val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager\n\n        val channelId = createHighPriorityNotificationChannel()\n\n        // Intent for \"I'm Okay\" action\n        val imOkayIntent = Intent(this, DisasterDetectionService::class.java).apply {\n            action = Constants.ACTION_IGNORE_ALERT\n        }\n        val imOkayPendingIntent = PendingIntent.getService(\n            this, 0, imOkayIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n        // Intent for \"Need Help\" action\n        val needHelpIntent = Intent(this, DisasterDetectionService::class.java).apply {\n            action = Constants.ACTION_START_SOS\n        }\n        val needHelpPendingIntent = PendingIntent.getService(\n            this, 1, needHelpIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n        // Build the notification\n        val notificationBuilder = NotificationCompat.Builder(this, channelId)\n            .setSmallIcon(R.drawable.ic_notification_alerting)\n            .setContentTitle(\"Are you okay?\")\n            .setContentText(\"Detected a possible disaster.\")\n            .setPriority(NotificationCompat.PRIORITY_HIGH)\n            .setCategory(NotificationCompat.CATEGORY_ALARM)\n            .setAutoCancel(true)\n            .addAction(R.drawable.ic_im_okay, \"I'm Okay\", imOkayPendingIntent)\n            .addAction(R.drawable.ic_need_help, \"Need Help\", needHelpPendingIntent)\n\n        // Show the notification\n        notificationManager.notify(Constants.DISASTER_RESPONSE_NOTIFICATION_ID, notificationBuilder.build())\n    }\n\n    private fun startConfirmationTimer() {\n        confirmationJob?.cancel()\n        confirmationJob = scope.launch {\n            delay((Constants.DISASTER_RESPONSE_GRACE_PERIOD * 1000).toLong())\n            startSOSService()\n        }\n    }\n\n    private fun cancelConfirmationTimer() {\n        confirmationJob?.cancel()\n        confirmationJob = null\n    }\n\n    private fun startSOSService() {\n        val sosIntent = Intent(this, SOSService::class.java)\n        startForegroundService(sosIntent)\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        cancelConfirmationTimer()\n        if (intent?.action != null) {\n            // Dismiss the alert notification\n            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager\n            notificationManager.cancel(Constants.DISASTER_RESPONSE_NOTIFICATION_ID)\n        }\n\n        when (intent?.action) {\n            Constants.ACTION_START_SOS -> {\n                // Start SOS immediately\n                startSOSService()\n            }\n        }\n\n        return START_STICKY\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        // Clean up resources\n        cancelConfirmationTimer()\n        emergencyManager.stopDetector()\n        scope.cancel()\n    }\n\n    override fun onBind(intent: Intent?): IBinder? {\n        return null\n    }\n\n    private fun createNotification(): Notification {\n        val channelId = createNotificationChannel()\n\n        val notificationBuilder = NotificationCompat.Builder(this, channelId)\n            .setContentTitle(\"Monitoring for disasters\")\n            .setSmallIcon(R.drawable.ic_notification)\n            .setPriority(NotificationCompat.PRIORITY_LOW)\n            .setOngoing(true)\n\n        return notificationBuilder.build()\n    }\n\n    private fun createNotificationChannel(): String {\n        val channelId = Constants.DISASTER_MONITORING_NOTIFICATION_KEY\n        val channelName = \"Disaster Detection Service\"\n        val channel = NotificationChannel(\n            channelId,\n            channelName,\n            NotificationManager.IMPORTANCE_LOW\n        )\n        val notificationManager = getSystemService(NotificationManager::class.java)\n        notificationManager.createNotificationChannel(channel)\n        return channelId\n    }\n\n    private fun createHighPriorityNotificationChannel(): String {\n        val channelId = Constants.DISASTER_RESPONSE_NOTIFICATION_KEY\n        val channelName = \"Emergency Alerts\"\n        val channel = NotificationChannel(\n            channelId,\n            channelName,\n            NotificationManager.IMPORTANCE_HIGH\n        )\n        channel.enableVibration(true)\n        channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC\n        val notificationManager = getSystemService(NotificationManager::class.java)\n        notificationManager.createNotificationChannel(channel)\n        return channelId\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/service/DisasterDetector.kt",
    "content": "package com.nizarmah.igatha.service\n\nimport android.content.Context\nimport com.nizarmah.igatha.Constants\nimport com.nizarmah.igatha.sensor.*\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport com.nizarmah.igatha.sensor.Sensor as InternalSensor\n\nclass DisasterDetector(\n    context: Context,\n    accelerationThreshold: Double,\n    rotationThreshold: Double,\n    pressureThreshold: Double,\n    private val eventTimeWindow: Long // in milliseconds\n) {\n\n    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())\n\n    private val eventTimes = mutableMapOf<SensorType, Long>()\n\n    private val accelerometerSensor = AcceleratorSensor(\n        context,\n        threshold = accelerationThreshold,\n        updateInterval = Constants.SENSOR_UPDATE_INTERVAL\n    )\n    private val gyroscopeSensor = GyroscopeSensor(\n        context,\n        threshold = rotationThreshold,\n        updateInterval = Constants.SENSOR_UPDATE_INTERVAL\n    )\n    private val barometerSensor = BarometerSensor(\n        context,\n        threshold = pressureThreshold,\n        updateInterval = Constants.SENSOR_UPDATE_INTERVAL\n    )\n\n    private val _isAvailable = MutableStateFlow(true)\n    val isAvailable: StateFlow<Boolean> = _isAvailable.asStateFlow()\n\n    private val _isActive = MutableStateFlow(false)\n    val isActive: StateFlow<Boolean> = _isActive.asStateFlow()\n\n    fun startDetection() {\n        scope.launch {\n            collectSensorEvents(accelerometerSensor)\n        }\n        scope.launch {\n            collectSensorEvents(gyroscopeSensor)\n        }\n        scope.launch {\n            collectSensorEvents(barometerSensor)\n        }\n\n        _isActive.value = true\n    }\n\n    fun stopDetection() {\n        _isActive.value = false\n\n        accelerometerSensor.stopUpdates()\n        gyroscopeSensor.stopUpdates()\n        barometerSensor.stopUpdates()\n        scope.coroutineContext.cancelChildren()\n    }\n\n    private suspend fun collectSensorEvents(sensor: InternalSensor) {\n        sensor.startUpdates()\n        sensor.events.collect { event ->\n            eventTimes[event.sensorType] = System.currentTimeMillis()\n            checkForIncident()\n        }\n    }\n\n    private fun checkForIncident() {\n        val currentTime = System.currentTimeMillis()\n\n        if (\n            (barometerSensor.isAvailable &&\n                eventTimes.size < 3) ||\n            // Some android devices don't have barometers\n            (!barometerSensor.isAvailable &&\n                eventTimes.size < 2)\n        ) return\n\n        if (\n            (barometerSensor.isAvailable &&\n                eventTimes.values.all { currentTime - it <= eventTimeWindow }) ||\n            // If no barometer, apply aggressive filtering\n            (!barometerSensor.isAvailable &&\n                eventTimes.values.all { currentTime - it <= eventTimeWindow / 3.2 })\n        ) {\n            // Apply more aggressive filtering if barometer is missing\n            if (\n                !barometerSensor.isAvailable &&\n                eventTimes.values.all { currentTime - it > eventTimeWindow / 2 }\n            ) {\n                return\n            }\n\n            // Disaster detected\n            scope.launch {\n                DisasterEventBus.emitDisasterDetected()\n            }\n            eventTimes.clear()\n        }\n    }\n\n    fun deinit() {\n        stopDetection()\n        scope.cancel()\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/service/DisasterEventBus.kt",
    "content": "package com.nizarmah.igatha.service\n\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\n\nobject DisasterEventBus {\n    private val _disasterDetectedFlow = MutableSharedFlow<Unit>(replay = 0)\n    val disasterDetectedFlow: SharedFlow<Unit> = _disasterDetectedFlow\n\n    suspend fun emitDisasterDetected() {\n        _disasterDetectedFlow.emit(Unit)\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/service/EmergencyManager.kt",
    "content": "package com.nizarmah.igatha.service\n\nimport android.content.Context\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.flow.*\nimport com.nizarmah.igatha.Constants\nimport com.nizarmah.igatha.util.PermissionsManager\nimport com.nizarmah.igatha.util.SettingsManager\nimport kotlinx.coroutines.cancel\n\nclass EmergencyManager private constructor(context: Context) {\n    private val appContext = context.applicationContext\n\n    companion object {\n        @Volatile\n        private var instance: EmergencyManager? = null\n\n        fun getInstance(ctx: Context?): EmergencyManager {\n            val appCtx = ctx?.applicationContext\n                ?: throw IllegalStateException(\"EmergencyManager requested with null Context\")\n\n            return instance ?: synchronized(this) {\n                instance ?: EmergencyManager(appCtx).also { instance = it }\n            }\n        }\n    }\n\n    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())\n\n    // Components initialization\n    private val sosBeacon = SOSBeacon(appContext)\n    private val sirenPlayer = SirenPlayer(appContext)\n    private val disasterDetector = DisasterDetector(\n        context = appContext,\n        accelerationThreshold = Constants.SENSOR_ACCELERATION_THRESHOLD,\n        rotationThreshold = Constants.SENSOR_ROTATION_THRESHOLD,\n        pressureThreshold = Constants.SENSOR_PRESSURE_THRESHOLD,\n        eventTimeWindow = Constants.DISASTER_TEMPORAL_CORRELATION_TIME_WINDOW.toLong()\n    )\n\n    // State management\n    val isSOSAvailable: StateFlow<Boolean> = combine(\n        sosBeacon.isAvailable,\n        sirenPlayer.isAvailable,\n        PermissionsManager.sosPermitted\n    ) { beacon, siren, permitted ->\n        beacon && siren && permitted\n    }.stateIn(scope, SharingStarted.Eagerly, false)\n\n    val isSOSActive: StateFlow<Boolean> = combine(\n        sosBeacon.isActive,\n        sirenPlayer.isActive\n    ) { beacon, siren ->\n        beacon || siren\n    }.stateIn(scope, SharingStarted.Eagerly, false)\n\n    val isDetectorAvailable: StateFlow<Boolean> = combine(\n        disasterDetector.isAvailable,\n        isSOSAvailable,\n        PermissionsManager.disasterDetectionPermitted\n    ) { detector, sos, permitted ->\n        detector && sos && permitted\n    }.stateIn(scope, SharingStarted.Eagerly, false)\n    val isDetectorEnabled: StateFlow<Boolean> = combine(\n        isDetectorAvailable,\n        SettingsManager.disasterDetectionEnabled\n    ) { available, enabled ->\n        available && enabled\n    }.stateIn(scope, SharingStarted.Eagerly, false)\n    val isDetectorActive: StateFlow<Boolean> = disasterDetector.isActive\n\n    // Core functionality\n    fun startSOS() {\n        if (!isSOSAvailable.value || isSOSActive.value) return\n\n        sosBeacon.startBroadcasting()\n        sirenPlayer.startSiren()\n    }\n\n    fun stopSOS() {\n        sosBeacon.stopBroadcasting()\n        sirenPlayer.stopSiren()\n    }\n\n    fun startDetector(): Boolean {\n        if (!isDetectorEnabled.value) return false\n\n        if (isDetectorActive.value) return true\n\n        disasterDetector.startDetection()\n        return true\n    }\n\n    fun stopDetector() {\n        disasterDetector.stopDetection()\n    }\n\n    fun deinit() {\n        stopSOS()\n        stopDetector()\n        sirenPlayer.deinit()\n        sosBeacon.deinit()\n        disasterDetector.deinit()\n        scope.cancel()\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/service/FeedbackWorker.kt",
    "content": "package com.nizarmah.igatha.service\n\nimport android.Manifest\nimport android.app.Notification\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.annotation.RequiresPermission\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.work.CoroutineWorker\nimport androidx.work.WorkerParameters\nimport com.nizarmah.igatha.Constants\nimport com.nizarmah.igatha.R\n\nclass FeedbackWorker(\n    context: Context,\n    workerParams: WorkerParameters\n) : CoroutineWorker(context, workerParams) {\n    @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)\n    override suspend fun doWork(): Result {\n        val channel = createNotificationChannel()\n\n        val notificationManager =\n            applicationContext.getSystemService(NotificationManager::class.java)\n\n        notificationManager?.createNotificationChannel(channel)\n\n        val notification = sendFeedbackNotification()\n\n        // Display the notification\n        NotificationManagerCompat.from(applicationContext)\n            .notify(Constants.FEEDBACK_NOTIFICATION_ID, notification)\n\n        return Result.success()\n    }\n\n    // createNotificationChannel creates or updates the notification channel for feedback\n    private fun createNotificationChannel(): NotificationChannel {\n        val channelId = Constants.FEEDBACK_NOTIFICATION_KEY\n        val channelName = \"Feedback Requests\"\n        val channel = NotificationChannel(\n            channelId,\n            channelName,\n            NotificationManager.IMPORTANCE_DEFAULT\n        )\n\n        return channel\n    }\n\n    // sendFeedbackNotification builds and displays the feedback notification\n    private fun sendFeedbackNotification(): Notification {\n        val channelId = Constants.FEEDBACK_NOTIFICATION_KEY\n\n        // Build deep link intent using the feedback notification link constant\n        val contentUri = Uri.Builder()\n            .scheme(Constants.DeepLink.SCHEME)\n            .authority(Constants.Notifications.Feedback.LINK.VALUE)\n            .build()\n\n        // Build the pending intent\n        val intent = Intent(Intent.ACTION_VIEW, contentUri)\n        val pendingIntent = PendingIntent.getActivity(\n            applicationContext,\n            0,\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n        // Build the notification\n        val notification = NotificationCompat.Builder(applicationContext, channelId)\n            .setSmallIcon(R.drawable.ic_notification)\n            .setContentTitle(\"Tell us why you use Igatha\")\n            .setContentText(\"Help us improve it for you and others\")\n            .setPriority(NotificationCompat.PRIORITY_DEFAULT)\n            .setAutoCancel(true)\n            .setContentIntent(pendingIntent)\n            .build()\n\n        return notification\n    }\n}"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/service/ProximityScanner.kt",
    "content": "package com.nizarmah.igatha.service\n\nimport android.bluetooth.BluetoothAdapter\nimport android.bluetooth.BluetoothManager\nimport android.bluetooth.le.BluetoothLeScanner\nimport android.bluetooth.le.ScanCallback\nimport android.bluetooth.le.ScanFilter\nimport android.bluetooth.le.ScanResult\nimport android.bluetooth.le.ScanSettings\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.util.Log\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport com.nizarmah.igatha.Constants\nimport com.nizarmah.igatha.model.Device\nimport java.util.Date\nimport java.util.UUID\n\nclass ProximityScanner(private val context: Context) {\n    private val _isActive = MutableStateFlow(false)\n    val isActive: StateFlow<Boolean> = _isActive.asStateFlow()\n\n    private val _isAvailable = MutableStateFlow(false)\n    val isAvailable: StateFlow<Boolean> = _isAvailable.asStateFlow()\n\n    private val _scannedDevices = MutableStateFlow<Device?>(null)\n    val scannedDevices: StateFlow<Device?> = _scannedDevices.asStateFlow()\n\n    private var bluetoothLeScanner: BluetoothLeScanner? = null\n\n    private val scanCallback = object : ScanCallback() {\n        override fun onScanResult(callbackType: Int, result: ScanResult) {\n            super.onScanResult(callbackType, result)\n\n            val macAddress = result.device.address\n            val uuid = UUID.nameUUIDFromBytes(macAddress.toByteArray())\n\n            val device = Device(\n                id = uuid,\n                rssi = result.rssi.toDouble(),\n                lastSeen = Date()\n            )\n\n            _scannedDevices.value = device\n        }\n\n        override fun onScanFailed(errorCode: Int) {\n            super.onScanFailed(errorCode)\n            Log.e(TAG, \"Scanning failed with error: $errorCode\")\n            stopScanning()\n        }\n    }\n\n    private val bluetoothStateReceiver = object : BroadcastReceiver() {\n        override fun onReceive(context: Context?, intent: Intent?) {\n            if (BluetoothAdapter.ACTION_STATE_CHANGED != intent?.action) return\n\n            updateAvailability()\n        }\n    }\n\n    init {\n        initialize()\n\n        // Register for Bluetooth state changes\n        val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)\n        context.registerReceiver(bluetoothStateReceiver, filter)\n    }\n\n    fun deinit() {\n        context.unregisterReceiver(bluetoothStateReceiver)\n        stopScanning()\n    }\n\n    private fun initialize() {\n        val bluetoothAdapter = getBluetoothAdapter()\n        if (bluetoothAdapter == null) {\n            _isAvailable.value = false\n            return\n        }\n\n        bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner\n        updateAvailability()\n    }\n\n    fun startScanning() {\n        if (!_isAvailable.value || _isActive.value) {\n            return\n        }\n\n        val scanFilters = listOf(\n            ScanFilter.Builder()\n                // TODO: Handle iOS overflow for bg advertisement\n                // See: http://www.davidgyoungtech.com/2020/05/07/hacking-the-overflow-area\n                .setServiceUuid(Constants.SOS_BEACON_SERVICE_UUID)\n                .build()\n        )\n\n        val scanSettings = ScanSettings.Builder()\n            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)\n            .build()\n\n        try {\n            bluetoothLeScanner?.startScan(scanFilters, scanSettings, scanCallback)\n            _isActive.value = true\n        } catch (e: SecurityException) {\n            Log.e(TAG, \"SecurityException in startScanning: ${e.message}\")\n            _isActive.value = false\n        }\n    }\n\n    fun stopScanning() {\n        if (!_isAvailable.value || !_isActive.value) return\n\n        try {\n            bluetoothLeScanner?.stopScan(scanCallback)\n        } catch (e: SecurityException) {\n            Log.e(TAG, \"SecurityException in stopScanning: ${e.message}\")\n        }\n\n        _isActive.value = false\n    }\n\n    private fun updateAvailability() {\n        val bluetoothAdapter = getBluetoothAdapter()\n        if (bluetoothAdapter == null) {\n            _isAvailable.value = false\n            return\n        }\n\n        try {\n            _isAvailable.value = bluetoothAdapter.isEnabled\n            if (!_isAvailable.value && _isActive.value) {\n                stopScanning()\n            }\n        } catch (e: SecurityException) {\n            _isAvailable.value = false\n            Log.e(TAG, \"SecurityException in updateAvailability: ${e.message}\")\n        }\n    }\n\n    private fun getBluetoothAdapter(): BluetoothAdapter? {\n        val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager\n        return bluetoothManager.adapter\n    }\n\n    companion object {\n        private const val TAG = \"ProximityScanner\"\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/service/SOSBeacon.kt",
    "content": "package com.nizarmah.igatha.service\n\nimport android.Manifest\nimport android.bluetooth.BluetoothAdapter\nimport android.bluetooth.BluetoothManager\nimport android.bluetooth.le.*\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport android.util.Log\nimport androidx.core.content.ContextCompat\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport com.nizarmah.igatha.Constants\n\nclass SOSBeacon(private val context: Context) {\n    // StateFlows to expose state changes\n    private val _isActive = MutableStateFlow(false)\n    val isActive: StateFlow<Boolean> = _isActive.asStateFlow()\n\n    private val _isAvailable = MutableStateFlow(false)\n    val isAvailable: StateFlow<Boolean> = _isAvailable.asStateFlow()\n\n    private var bluetoothLeAdvertiser: BluetoothLeAdvertiser? = null\n\n    private val advertiseCallback = object : AdvertiseCallback() {\n        override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {\n            super.onStartSuccess(settingsInEffect)\n\n            _isActive.value = true\n        }\n\n        override fun onStartFailure(errorCode: Int) {\n            super.onStartFailure(errorCode)\n\n            _isActive.value = false\n        }\n    }\n\n    private val bluetoothStateReceiver = object : BroadcastReceiver() {\n        override fun onReceive(context: Context?, intent: Intent?) {\n            if (BluetoothAdapter.ACTION_STATE_CHANGED != intent?.action) return\n\n            updateAvailability()\n        }\n    }\n\n    init {\n        initialize()\n\n        // Register the Bluetooth state receiver\n        val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)\n        context.registerReceiver(bluetoothStateReceiver, filter)\n    }\n\n    fun deinit() {\n        context.unregisterReceiver(bluetoothStateReceiver)\n\n        stopBroadcasting()\n    }\n\n    private fun initialize() {\n        val bluetoothAdapter = getBluetoothAdapter()\n        if (bluetoothAdapter == null) {\n            _isAvailable.value = false\n            return\n        }\n\n        bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser\n\n        updateAvailability()\n    }\n\n    fun startBroadcasting() {\n        if (!_isAvailable.value || _isActive.value) {\n            return\n        }\n\n        val settings = AdvertiseSettings.Builder()\n            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)\n            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)\n            .setConnectable(false)\n            .setTimeout(0)\n            .build()\n\n        val advertiseData = AdvertiseData.Builder()\n            .addServiceUuid(Constants.SOS_BEACON_SERVICE_UUID)\n            .setIncludeDeviceName(false)\n            .build()\n\n        try {\n            bluetoothLeAdvertiser?.startAdvertising(settings, advertiseData, advertiseCallback)\n        } catch (e: SecurityException) {\n            _isActive.value = false\n\n            Log.e(TAG, \"SecurityException in startBroadcasting: ${e.message}\")\n        }\n    }\n\n    fun stopBroadcasting() {\n        try {\n            bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback)\n        } catch (e: SecurityException) {\n            Log.e(TAG, \"SecurityException in stopBroadcasting: ${e.message}\")\n        }\n\n        _isActive.value = false\n    }\n\n    private fun updateAvailability() {\n        val bluetoothAdapter = getBluetoothAdapter()\n        if (bluetoothAdapter == null) {\n            _isAvailable.value = false\n            return\n        }\n\n        val isEnabled: Boolean\n        val isSupported: Boolean\n\n        try {\n            isEnabled = bluetoothAdapter.isEnabled\n            isSupported = bluetoothAdapter.isMultipleAdvertisementSupported\n        } catch (e: SecurityException) {\n            _isAvailable.value = false\n\n            Log.e(TAG, \"SecurityException in updateAvailability: ${e.message}\")\n\n            return\n        }\n\n        _isAvailable.value = isEnabled && isSupported\n        if (!_isAvailable.value && _isActive.value) {\n            stopBroadcasting()\n        }\n    }\n\n    private fun getBluetoothAdapter(): BluetoothAdapter? {\n        if (!hasBluetoothPermissions()) {\n            Log.e(TAG, \"Missing Bluetooth permissions\")\n            return null\n        }\n\n        val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager\n        val bluetoothAdapter = bluetoothManager.adapter\n\n        if (bluetoothAdapter == null) {\n            Log.e(TAG, \"Bluetooth is not supported on this device\")\n            return null\n        }\n\n        return bluetoothAdapter\n    }\n\n    private fun hasBluetoothPermissions(): Boolean {\n        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n            // For Android 12 and above\n            ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADVERTISE) == PackageManager.PERMISSION_GRANTED &&\n            ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED\n        } else {\n            // For below Android 12\n            true // Permissions are granted at install time\n        }\n    }\n\n    companion object {\n        private const val TAG = \"SOSBeacon\"\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/service/SOSService.kt",
    "content": "package com.nizarmah.igatha.service\n\nimport android.app.*\nimport android.content.Intent\nimport android.os.IBinder\nimport androidx.core.app.NotificationCompat\nimport com.nizarmah.igatha.Constants\nimport com.nizarmah.igatha.R\nimport kotlinx.coroutines.*\n\nclass SOSService : Service() {\n\n    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())\n\n    private lateinit var emergencyManager: EmergencyManager\n\n    override fun onCreate() {\n        super.onCreate()\n\n        // Initialize inside onCreate because context is not available earlier\n        emergencyManager = EmergencyManager.getInstance(applicationContext)\n\n        startSOS()\n    }\n\n    private fun startSOS() {\n        emergencyManager.startSOS()\n\n        // Show notification with \"Stop SOS\" action\n        showSOSNotification()\n    }\n\n    private fun stopSOS() {\n        emergencyManager.stopSOS()\n\n        // Cancel SOS notification\n        val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager\n        notificationManager.cancel(Constants.DISTRESS_ACTIVE_NOTIFICATION_ID)\n\n        // Stop the service\n        stopSelf()\n    }\n\n    private fun showSOSNotification() {\n        val channelId = createNotificationChannel()\n\n        // Intent to stop SOS\n        val stopSOSIntent = Intent(this, SOSService::class.java).apply {\n            action = Constants.ACTION_STOP_SOS\n        }\n        val stopSOSPendingIntent = PendingIntent.getService(\n            this, 0, stopSOSIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n        // Build the notification\n        val notificationBuilder = NotificationCompat.Builder(this, channelId)\n            .setSmallIcon(R.drawable.ic_notification_signaling)\n            .setContentTitle(\"Broadcasting SOS\")\n            .setPriority(NotificationCompat.PRIORITY_HIGH)\n            .setOngoing(true)\n            .addAction(R.drawable.ic_stop_sos, \"Stop\", stopSOSPendingIntent)\n\n        // Start the service in the foreground\n        startForeground(Constants.DISTRESS_ACTIVE_NOTIFICATION_ID, notificationBuilder.build())\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        when (intent?.action) {\n            Constants.ACTION_STOP_SOS -> {\n                // Stop SOS procedures\n                stopSOS()\n            }\n        }\n\n        return START_STICKY\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        // Clean up resources\n        stopSOS()\n        scope.cancel()\n    }\n\n    override fun onBind(intent: Intent?): IBinder? {\n        return null\n    }\n\n    private fun createNotificationChannel(): String {\n        val channelId = Constants.DISTRESS_ACTIVE_NOTIFICATION_KEY\n        val channelName = \"SOS Service\"\n        val channel = NotificationChannel(\n            channelId,\n            channelName,\n            NotificationManager.IMPORTANCE_HIGH\n        )\n        val notificationManager = getSystemService(NotificationManager::class.java)\n        notificationManager.createNotificationChannel(channel)\n        return channelId\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/service/SirenPlayer.kt",
    "content": "package com.nizarmah.igatha.service\n\nimport android.content.Context\nimport android.media.*\nimport android.util.Log\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlin.math.PI\nimport kotlin.math.sin\n\nclass SirenPlayer(context: Context) {\n    // StateFlows for isActive and isAvailable\n    private val _isActive = MutableStateFlow(false)\n    val isActive: StateFlow<Boolean> = _isActive.asStateFlow()\n\n    private val _isAvailable = MutableStateFlow(false)\n    val isAvailable: StateFlow<Boolean> = _isAvailable.asStateFlow()\n\n    private var audioTrack: AudioTrack? = null\n    private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager\n\n    init {\n        // assume supported\n        _isAvailable.value = true\n    }\n\n    fun deinit() {\n        stopSiren()\n    }\n\n    fun startSiren() {\n        if (!_isAvailable.value || _isActive.value) return\n\n        try {\n            val audioAttributes = AudioAttributes.Builder()\n                .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)\n                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)\n                .build()\n\n            val sampleRate = 44100\n            val buffer = createSirenBuffer()\n\n            audioTrack = AudioTrack.Builder()\n                .setAudioAttributes(audioAttributes)\n                .setAudioFormat(\n                    AudioFormat.Builder()\n                        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)\n                        .setSampleRate(sampleRate)\n                        .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)\n                        .build()\n                )\n                .setTransferMode(AudioTrack.MODE_STATIC)\n                .setBufferSizeInBytes(buffer.size * 2)\n                .build()\n\n            // Route audio to built-in speaker\n            audioManager.mode = AudioManager.MODE_IN_COMMUNICATION\n            audioManager.isSpeakerphoneOn = true\n\n            val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)\n            val speakerDevice = devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }\n            if (speakerDevice != null) {\n                audioTrack?.preferredDevice = speakerDevice\n            } else {\n                Log.e(TAG, \"Speaker device not found\")\n            }\n\n            audioTrack?.let { track ->\n                track.write(buffer, 0, buffer.size)\n                track.setLoopPoints(0, buffer.size / track.channelCount, -1)\n                track.play()\n                _isActive.value = true\n                Log.d(TAG, \"Siren started\")\n            } ?: run {\n                _isAvailable.value = false\n                Log.e(TAG, \"Failed to create AudioTrack\")\n            }\n        } catch (e: Exception) {\n            _isAvailable.value = false\n            Log.e(TAG, \"Exception in startSiren: ${e.message}\", e)\n        }\n    }\n\n    fun stopSiren() {\n        try {\n            // Reset audio mode and routing\n            audioManager.mode = AudioManager.MODE_NORMAL\n            audioManager.isSpeakerphoneOn = false\n\n            audioTrack?.let { track ->\n                track.stop()\n                track.release()\n                Log.d(TAG, \"Siren stopped\")\n            }\n            audioTrack = null\n            _isActive.value = false\n        } catch (e: Exception) {\n            Log.e(TAG, \"Exception in stopSiren: ${e.message}\", e)\n        }\n    }\n\n    private fun createSirenBuffer(): ShortArray {\n        return try {\n            val duration = 2.0 // seconds\n            val sampleRate = 44100 // Hz\n            val totalSamples = (sampleRate * duration).toInt()\n            val buffer = ShortArray(totalSamples)\n\n            val frequencyStart = 600.0f // Hz\n            val frequencyEnd = 1200.0f // Hz\n            val amplitude = 0.8f // Volume (0.0 to 1.0)\n\n            for (sampleIndex in 0 until totalSamples) {\n                val t = sampleIndex / sampleRate.toFloat()\n\n                // Modulate frequency to create siren effect\n                val modulation = sin(PI.toFloat() * t / duration.toFloat())\n\n                val frequency = frequencyStart + modulation * (frequencyEnd - frequencyStart)\n                val sample = sin(2.0f * PI.toFloat() * frequency * t) * amplitude\n\n                // Convert to 16-bit PCM value\n                val pcmValue = (sample * Short.MAX_VALUE).toInt().toShort()\n                buffer[sampleIndex] = pcmValue\n            }\n\n            buffer\n        } catch (e: Exception) {\n            Log.e(TAG, \"Exception in createSirenBuffer: ${e.message}\", e)\n            _isAvailable.value = false\n            ShortArray(0)\n        }\n    }\n\n    companion object {\n        private const val TAG = \"SirenPlayer\"\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/component/FeedbackButtonView.kt",
    "content": "package com.nizarmah.igatha.ui.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material.icons.outlined.FavoriteBorder\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.nizarmah.igatha.ui.theme.IgathaTheme\nimport com.nizarmah.igatha.ui.theme.colors\n\n/**\n * A styled button for navigation to the feedback form.\n * Matches the iOS design with gradient background and heart icon.\n */\n@Composable\nfun FeedbackButtonView(\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    // Create a pink-to-purple gradient to match iOS\n    val gradient = Brush.linearGradient(\n        colors = listOf(\n            MaterialTheme.colors.pink.copy(alpha = 0.6f),\n            MaterialTheme.colors.purple.copy(alpha = 0.6f)\n        ),\n        start = Offset.Zero,\n        end = Offset.Infinite\n    )\n\n    Surface(\n        modifier = modifier\n            .fillMaxWidth()\n            .clip(RoundedCornerShape(4.dp))\n            .clickable { onClick() },\n        shadowElevation = 4.dp\n    ) {\n        Row(\n            modifier = Modifier\n                .background(gradient)\n                .padding(vertical = 16.dp, horizontal = 20.dp)\n                .fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(16.dp)\n            ) {\n                // Heart outline icon\n                Icon(\n                    imageVector = Icons.Outlined.FavoriteBorder,\n                    contentDescription = \"Feedback\",\n                    modifier = Modifier.size(30.dp),\n                    tint = Color.White\n                )\n\n                // Text content\n                Column(\n                    verticalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    Text(\n                        text = \"Tell us why you use Igatha\",\n                        style = MaterialTheme.typography.titleMedium,\n                        fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,\n                        color = Color.White\n                    )\n\n                    Text(\n                        text = \"It helps us make it more reliable.\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = Color.White.copy(alpha = 0.9f)\n                    )\n                }\n            }\n\n            // Chevron\n            Icon(\n                imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,\n                contentDescription = \"Go to feedback form\",\n                tint = Color.White\n            )\n        }\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nfun FeedbackButtonViewPreview() {\n    IgathaTheme {\n        Box(\n            modifier = Modifier.padding(16.dp)\n        ) {\n            FeedbackButtonView(\n                onClick = {}\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/component/PermissionHandler.kt",
    "content": "package com.nizarmah.igatha.ui.component\n\nimport android.bluetooth.BluetoothAdapter\nimport android.bluetooth.BluetoothManager\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.provider.Settings\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport com.nizarmah.igatha.util.PermissionsHelper\nimport com.nizarmah.igatha.util.PermissionsManager\n\n@Composable\nfun PermissionHandler(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n    val lifecycleOwner = LocalLifecycleOwner.current\n\n    val permissionsGranted by PermissionsManager.permissionsGranted.collectAsState()\n\n    // Launcher to request multiple permissions\n    val permissionLauncher = rememberLauncherForActivityResult(\n        contract = ActivityResultContracts.RequestMultiplePermissions(),\n        onResult = { permissions ->\n            PermissionsManager.refreshPermissions(context)\n        }\n    )\n\n    // Check Bluetooth status and prompt if disabled\n    val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager\n    val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter\n    var isBluetoothEnabled by remember { mutableStateOf(bluetoothAdapter?.isEnabled == true) }\n\n    // Request permissions on first launch\n    LaunchedEffect(Unit) {\n        if (!permissionsGranted) {\n            permissionLauncher.launch(\n                PermissionsHelper.getSOSPermissions()\n                        + PermissionsHelper.getProximityScanPermissions()\n                        + PermissionsHelper.getDisasterDetectionPermissions()\n            )\n        }\n    }\n\n    // Monitor Bluetooth state changes\n    DisposableEffect(Unit) {\n        val receiver = object : android.content.BroadcastReceiver() {\n            override fun onReceive(context: Context?, intent: Intent?) {\n                if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {\n                    val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)\n                    isBluetoothEnabled = state == BluetoothAdapter.STATE_ON\n                }\n            }\n        }\n\n        val filter = android.content.IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)\n        context.registerReceiver(receiver, filter)\n\n        // Lifecycle observer for onResume\n        val observer = LifecycleEventObserver { _, event ->\n            if (event == Lifecycle.Event.ON_RESUME) {\n                // Refresh permissions and Bluetooth status\n                PermissionsManager.refreshPermissions(context)\n                isBluetoothEnabled = bluetoothAdapter?.isEnabled == true\n            }\n        }\n        lifecycleOwner.lifecycle.addObserver(observer)\n\n        onDispose {\n            context.unregisterReceiver(receiver)\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n\n    Column(\n        modifier = modifier\n    ) {\n        if (!permissionsGranted) {\n            PersistentBanner(\n                message = \"Permissions are missing.\",\n                actionLabel = \"Settings\",\n                onActionClick = {\n                    // Open app settings\n                    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {\n                        data = Uri.fromParts(\"package\", context.packageName, null)\n                        flags = Intent.FLAG_ACTIVITY_NEW_TASK\n                    }\n\n                    context.startActivity(intent)\n                }\n            )\n        } else if (!isBluetoothEnabled) {\n            PersistentBanner(\n                message = \"Bluetooth is required.\",\n                actionLabel = \"Enable\",\n                onActionClick = {\n                    // Open bluetooth settings\n                    val intent = Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply {\n                        flags = Intent.FLAG_ACTIVITY_NEW_TASK\n                    }\n\n                    context.startActivity(intent)\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/component/PersistentBanner.kt",
    "content": "package com.nizarmah.igatha.ui.component\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun PersistentBanner(\n    message: String,\n    actionLabel: String,\n    onActionClick: () -> Unit,\n    backgroundColor: Color = Color(0xFF323232),\n    messageColor: Color = Color.White,\n    actionTextColor: Color = Color(0xFFBB86FC)\n) {\n    Surface(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 0.dp, vertical = 0.dp), // Minimal external padding\n        color = backgroundColor, // Directly set the background color\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp, vertical = 8.dp), // Internal padding for content\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.SpaceBetween\n        ) {\n            Text(\n                text = message,\n                style = MaterialTheme.typography.bodyMedium,\n                color = messageColor\n            )\n            TextButton(onClick = onActionClick) {\n                Text(\n                    text = actionLabel,\n                    color = actionTextColor\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/component/Section.kt",
    "content": "package com.nizarmah.igatha.ui.component\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun Section(\n    header: String? = null,\n    footer: String? = null,\n    padding: Modifier = Modifier.padding(vertical = 8.dp),\n    content: @Composable () -> Unit,\n) {\n    Column(modifier = padding) {\n        header?.let {\n            SectionHeader(text = it)\n        }\n\n        Surface(\n            color = MaterialTheme.colorScheme.surface,\n            shape = MaterialTheme.shapes.medium,\n            modifier = Modifier\n                .padding(horizontal = 16.dp)\n                .padding(vertical = 0.dp)\n        ) {\n            Column {\n                content()\n            }\n        }\n\n        footer?.let {\n            SectionFooter(text = it)\n        }\n    }\n}\n\n@Composable\nfun SectionHeader(text: String) {\n    Text(\n        text = text.uppercase(),\n        style = MaterialTheme.typography.bodySmall.copy(\n            lineHeight = MaterialTheme.typography.bodySmall.fontSize.times(1.4f)\n        ),\n        color = MaterialTheme.colorScheme.secondary,\n        modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp)\n    )\n}\n\n@Composable\nfun SectionFooter(text: String) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.bodySmall.copy(\n            lineHeight = MaterialTheme.typography.bodySmall.fontSize.times(1.4f)\n        ),\n        color = MaterialTheme.colorScheme.secondary,\n        modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp)\n    )\n}\n\n@Composable\nfun SectionItem(\n    content: @Composable RowScope.() -> Unit\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp, vertical = 12.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.SpaceBetween\n    ) {\n        content()\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/screen/ContentScreen.kt",
    "content": "package com.nizarmah.igatha.ui.screen\n\nimport android.net.Uri\nimport androidx.compose.animation.AnimatedContentTransitionScope\nimport androidx.compose.animation.core.tween\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport androidx.navigation.NavType\nimport androidx.navigation.navArgument\nimport androidx.navigation.navDeepLink\nimport com.google.gson.Gson\nimport com.nizarmah.igatha.Constants\nimport com.nizarmah.igatha.model.Device\nimport com.nizarmah.igatha.ui.view.ContentView\nimport com.nizarmah.igatha.ui.view.DeviceDetailView\nimport com.nizarmah.igatha.viewmodel.ContentViewModel\n\n@Composable\nfun ContentScreen(modifier: Modifier) {\n    val navController = rememberNavController()\n    val viewModel: ContentViewModel = viewModel()\n\n    val isSOSAvailable by viewModel.isSOSAvailable.collectAsState()\n    val isSOSActive by viewModel.isSOSActive.collectAsState()\n    val activeAlert by viewModel.activeAlert.collectAsState()\n    val devices by viewModel.devices.collectAsState()\n\n    NavHost(\n        modifier = modifier,\n        navController = navController,\n        startDestination = \"home\",\n        enterTransition = {\n            slideIntoContainer(\n                towards = AnimatedContentTransitionScope.SlideDirection.Left,\n                animationSpec = tween(300)\n            )\n        },\n        exitTransition = {\n            slideOutOfContainer(\n                towards = AnimatedContentTransitionScope.SlideDirection.Left,\n                animationSpec = tween(300)\n            )\n        },\n        popEnterTransition = {\n            slideIntoContainer(\n                towards = AnimatedContentTransitionScope.SlideDirection.Right,\n                animationSpec = tween(300)\n            )\n        },\n        popExitTransition = {\n            slideOutOfContainer(\n                towards = AnimatedContentTransitionScope.SlideDirection.Right,\n                animationSpec = tween(300)\n            )\n        }\n    ) {\n        composable(\"home\") {\n            ContentView(\n                isSOSAvailable = isSOSAvailable,\n                isSOSActive = isSOSActive,\n                devices = devices,\n                activeAlert = activeAlert,\n                onSOSClick = {\n                    if (isSOSActive) {\n                        viewModel.stopSOS()\n                    } else {\n                        viewModel.showSOSConfirmation()\n                    }\n                },\n                onConfirmSOS = {\n                    viewModel.dismissAlert()\n                    viewModel.startSOS()\n                },\n                onDismissAlert = {\n                    viewModel.dismissAlert()\n                },\n                onSettingsClick = {\n                    navController.navigate(\"settings\")\n                },\n                onDeviceClick = { device ->\n                    val deviceJson = Gson().toJson(device)\n                    navController.navigate(\"device/$deviceJson\")\n                }\n            )\n        }\n\n        composable(\n            route = \"settings\",\n            deepLinks = listOf(\n                navDeepLink {\n                    uriPattern = Uri.Builder()\n                        .scheme(Constants.DeepLink.SCHEME)\n                        .authority(Constants.DeepLink.Settings.VALUE)\n                        .build().toString()\n                }\n            )\n        ) {\n            SettingsScreen(\n                onBackClick = {\n                    navController.popBackStack()\n                },\n                onFeedbackClick = {\n                    navController.navigate(\"feedback\")\n                }\n            )\n        }\n\n        composable(\"feedback\") {\n            FeedbackFormScreen(\n                onNavigateBack = {\n                    navController.popBackStack()\n                }\n            )\n        }\n\n        composable(\n            route = \"device/{device}\",\n            arguments = listOf(\n                navArgument(\"device\") {\n                    type = NavType.StringType\n                }\n            )\n        ) { backStackEntry ->\n            val deviceJson = backStackEntry.arguments?.getString(\"device\")\n            val device = Gson().fromJson(deviceJson, Device::class.java)\n\n            DeviceDetailView(\n                device = device,\n                onBackClick = {\n                    navController.popBackStack()\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/screen/FeedbackFormScreen.kt",
    "content": "package com.nizarmah.igatha.ui.screen\n\nimport android.app.Application\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport com.nizarmah.igatha.ui.view.FeedbackFormView\nimport com.nizarmah.igatha.viewmodel.FeedbackFormViewModel\nimport com.nizarmah.igatha.viewmodel.FeedbackFormViewModelFactory\nimport com.nizarmah.igatha.viewmodel.FormState\nimport com.nizarmah.igatha.viewmodel.SubmissionResult\nimport kotlinx.coroutines.launch\n\n/**\n * Feedback form screen to understand why people use Igatha.\n * This screen is responsible for coordination between the view and viewmodel.\n */\n@Composable\nfun FeedbackFormScreen(\n    onNavigateBack: () -> Unit\n) {\n    // Create ViewModel with Factory\n    val application = LocalContext.current.applicationContext as Application\n    val viewModel: FeedbackFormViewModel = viewModel(\n        factory = FeedbackFormViewModelFactory(application)\n    )\n\n    // Collect state from ViewModel\n    val formState by viewModel.formState.collectAsState()\n    val submissionResult by viewModel.submissionResult.collectAsState()\n    val hasCustomUsage by viewModel.hasCustomUsage.collectAsState()\n    val customUsage by viewModel.customUsage.collectAsState()\n    val ideas by viewModel.ideas.collectAsState()\n    val email by viewModel.email.collectAsState()\n\n    // For actions that need coroutine scope\n    val scope = rememberCoroutineScope()\n\n    // Use the FeedbackFormView from ui.view\n    FeedbackFormView(\n        formState = formState,\n        customUsage = customUsage,\n        ideas = ideas,\n        email = email,\n        hasCustomUsage = hasCustomUsage,\n        isUsageReasonSelected = { viewModel.isUsageReasonSelected(it) },\n        onUsageReasonToggle = { viewModel.toggleUsageReason(it) },\n        onCustomUsageChange = { viewModel.updateCustomUsage(it) },\n        onIdeasChange = { viewModel.updateIdeas(it) },\n        onEmailChange = { viewModel.updateEmail(it) },\n        onSubmit = {\n            if (formState == FormState.Idle) {\n                scope.launch {\n                    viewModel.submit()\n                }\n            }\n        },\n        onBackClick = onNavigateBack\n    )\n\n    // Alert dialogs for submission results\n    submissionResult?.let { result ->\n        when (result) {\n            is SubmissionResult.Success -> {\n                AlertDialog(\n                    onDismissRequest = {\n                        viewModel.dismissAlert()\n                        onNavigateBack()\n                    },\n                    title = { Text(\"Thank you!\") },\n                    text = { Text(\"Your feedback helps us improve Igatha.\") },\n                    confirmButton = {\n                        TextButton(\n                            onClick = {\n                                viewModel.dismissAlert()\n                                onNavigateBack()\n                            }\n                        ) {\n                            Text(\"Done\", color = MaterialTheme.colorScheme.primary)\n                        }\n                    }\n                )\n            }\n            is SubmissionResult.Error -> {\n                AlertDialog(\n                    onDismissRequest = { viewModel.dismissAlert() },\n                    title = { Text(\"Error\") },\n                    text = { Text(result.message) },\n                    confirmButton = {\n                        TextButton(onClick = { viewModel.dismissAlert() }) {\n                            Text(\"OK\", color = MaterialTheme.colorScheme.primary)\n                        }\n                    }\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/screen/SettingsScreen.kt",
    "content": "package com.nizarmah.igatha.ui.screen\n\nimport android.app.Application\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport com.nizarmah.igatha.ui.view.SettingsView\nimport com.nizarmah.igatha.viewmodel.SettingsViewModel\nimport com.nizarmah.igatha.viewmodel.SettingsViewModelFactory\n\n@Composable\nfun SettingsScreen(\n    onBackClick: () -> Unit,\n    onFeedbackClick: () -> Unit\n) {\n    val context = LocalContext.current.applicationContext as Application\n    val viewModel: SettingsViewModel = viewModel(factory = SettingsViewModelFactory(context))\n    val disasterDetectionEnabled by viewModel.disasterDetectionEnabled.collectAsState()\n    val isDisasterDetectionAvailable by viewModel.isDisasterDetectionAvailable.collectAsState()\n\n    SettingsView(\n        disasterDetectionEnabled = disasterDetectionEnabled,\n        isDisasterDetectionAvailable = isDisasterDetectionAvailable,\n        onDisasterDetectionEnabledChanged = { enabled ->\n            viewModel.setDisasterDetectionEnabled(enabled)\n        },\n        onBackClick = onBackClick,\n        onFeedbackClick = onFeedbackClick\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/theme/Color.kt",
    "content": "package com.nizarmah.igatha.ui.theme\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.ReadOnlyComposable\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.graphics.Color\n\nval Gray = Color(0xFF8E8E93)\n\n// iOS colors\n// @see https://developer.apple.com/design/human-interface-guidelines/color#iOS-iPadOS-system-colors\n\nval RedLight = Color(255, 59, 48)\nval RedDark = Color(255, 69, 58)\n\nval PinkLight = Color(255, 45, 85)\nval PinkDark = Color(255, 55, 95)\n\nval PurpleLight = Color(175, 82, 222)\nval PurpleDark = Color(191, 90, 242)\n\n/**\n * Data class representing iOS color palette with light/dark variants\n */\ndata class ColorScheme(\n    val red: Color = RedLight,\n    val pink: Color = PinkLight,\n    val purple: Color = PurpleLight\n)\n\n/**\n * Light mode iOS colors\n */\nval LightColors = ColorScheme(\n    red = RedLight,\n    pink = PinkLight,\n    purple = PurpleLight\n)\n\n/**\n * Dark mode iOS colors\n */\nval DarkColors = ColorScheme(\n    red = RedDark,\n    pink = PinkDark,\n    purple = PurpleDark\n)\n\n/**\n * CompositionLocal to provide iOS colors down the tree\n */\nval LocalColors = staticCompositionLocalOf { LightColors }\n\n/**\n * Extension property for accessing iOS colors from MaterialTheme\n */\nval MaterialTheme.colors: ColorScheme\n    @Composable\n    @ReadOnlyComposable\n    get() = LocalColors.current\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/theme/Theme.kt",
    "content": "package com.nizarmah.igatha.ui.theme\n\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.platform.LocalContext\n\nprivate val BaseLightColorScheme = lightColorScheme()\n\nprivate val BaseDarkColorScheme = darkColorScheme()\n\nprivate val LightColorScheme = BaseLightColorScheme.copy(\n    primary = RedLight,\n)\n\nprivate val DarkColorScheme = BaseDarkColorScheme.copy(\n    primary = RedDark,\n)\n\n@Composable\nfun IgathaTheme(\n    darkTheme: Boolean = isSystemInDarkTheme(),\n    // Dynamic color is available on Android 12+\n    dynamicColor: Boolean = true,\n    content: @Composable () -> Unit\n) {\n    val colorScheme = when {\n        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {\n            val context = LocalContext.current\n            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n        }\n\n        darkTheme -> DarkColorScheme\n        else -> LightColorScheme\n    }\n\n    // Select the appropriate iOS colors based on theme\n    val colors = if (darkTheme) DarkColors else LightColors\n\n    // Provide both Material and iOS colors down the tree\n    CompositionLocalProvider(\n        LocalColors provides colors\n    ) {\n        MaterialTheme(\n            colorScheme = colorScheme,\n            typography = Typography,\n            content = content\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/theme/Type.kt",
    "content": "package com.nizarmah.igatha.ui.theme\n\nimport androidx.compose.material3.Typography\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.sp\n\n// Set of Material typography styles to start with\nval Typography = Typography(\n    bodyLarge = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 16.sp,\n        lineHeight = 24.sp,\n        letterSpacing = 0.5.sp\n    )\n    /* Other default text styles to override\n    titleLarge = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 22.sp,\n        lineHeight = 28.sp,\n        letterSpacing = 0.sp\n    ),\n    labelSmall = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Medium,\n        fontSize = 11.sp,\n        lineHeight = 16.sp,\n        letterSpacing = 0.5.sp\n    )\n    */\n)"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/view/ContentView.kt",
    "content": "package com.nizarmah.igatha.ui.view\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Settings\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.nizarmah.igatha.ui.theme.Gray\nimport com.nizarmah.igatha.ui.theme.IgathaTheme\nimport com.nizarmah.igatha.model.Device\nimport com.nizarmah.igatha.viewmodel.AlertType\nimport java.util.Date\nimport java.util.UUID\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ContentView(\n    isSOSAvailable: Boolean,\n    isSOSActive: Boolean,\n    devices: List<Device>,\n    activeAlert: AlertType?,\n    onSOSClick: () -> Unit,\n    onConfirmSOS: () -> Unit,\n    onDismissAlert: () -> Unit,\n    onSettingsClick: () -> Unit,\n    onDeviceClick: (Device) -> Unit\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = {},\n                actions = {\n                    IconButton(onClick = onSettingsClick) {\n                        Icon(\n                            imageVector = Icons.Default.Settings,\n                            contentDescription = \"Settings\"\n                        )\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainer\n                )\n            )\n        }\n    ) { paddingValues ->\n        Column(\n            modifier = Modifier\n                .padding(paddingValues)\n                .fillMaxSize()\n        ) {\n            Box(\n                modifier = Modifier\n                    .weight(1f)\n                    .fillMaxWidth()\n            ) {\n                DeviceListView(\n                    devices = devices,\n                    onDeviceClick = onDeviceClick\n                )\n            }\n\n            SOSButton(\n                isSOSAvailable = isSOSAvailable,\n                isSOSActive = isSOSActive,\n                onSOSClick = onSOSClick\n            )\n        }\n    }\n\n    // Handle alerts\n    activeAlert?.let { alert ->\n        when (alert) {\n            is AlertType.SOSConfirmation -> {\n                AlertDialog(\n                    onDismissRequest = onDismissAlert,\n                    title = { Text(\"Are you sure?\") },\n                    text = {\n                        Text(\n                            text = \"This will broadcast your location and start a loud siren.\",\n                            lineHeight = MaterialTheme.typography.bodyMedium.fontSize.times(1.4f)\n                        )\n                    },\n                    confirmButton = {\n                        TextButton(\n                            onClick = onConfirmSOS\n                        ) {\n                            Text(\"Yes\")\n                        }\n                    },\n                    dismissButton = {\n                        TextButton(\n                            onClick = onDismissAlert\n                        ) {\n                            Text(\"Cancel\")\n                        }\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun SOSButton(\n    isSOSAvailable: Boolean = true,\n    isSOSActive: Boolean = false,\n    onSOSClick: () -> Unit = {}\n) {\n    Button(\n        onClick = onSOSClick,\n        enabled = isSOSAvailable,\n        modifier = Modifier\n            .padding(16.dp)\n            .fillMaxWidth()\n            .height(50.dp)\n            .alpha(if (isSOSAvailable) 1f else 0.75f),\n        shape = RoundedCornerShape(10.dp),\n        colors = ButtonDefaults.buttonColors(\n            containerColor = when {\n                isSOSActive -> Gray\n                else -> MaterialTheme.colorScheme.primary\n            },\n            contentColor = Color.White,\n            disabledContainerColor = MaterialTheme.colorScheme.primary,\n            disabledContentColor = Color.White\n        )\n    ) {\n        Text(\n            text = when {\n                !isSOSAvailable -> \"SOS Unavailable\"\n                isSOSActive -> \"Stop SOS\"\n                else -> \"Send SOS\"\n            },\n            fontWeight = FontWeight.Bold\n        )\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nfun ContentViewPreview() {\n    IgathaTheme {\n        ContentView(\n            isSOSAvailable = true,\n            isSOSActive = false,\n            devices = listOf(\n                Device(\n                    id = UUID.randomUUID(),\n                    rssi = -75.0,\n                    lastSeen = Date()\n                ),\n                Device(\n                    id = UUID.randomUUID(),\n                    rssi = -85.0,\n                    lastSeen = Date()\n                )\n            ),\n            activeAlert = null,\n            onSOSClick = {},\n            onConfirmSOS = {},\n            onDismissAlert = {},\n            onSettingsClick = {},\n            onDeviceClick = { device -> }\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/view/DeviceDetailView.kt",
    "content": "package com.nizarmah.igatha.ui.view\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.sharp.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.nizarmah.igatha.model.Device\nimport com.nizarmah.igatha.ui.theme.IgathaTheme\nimport com.nizarmah.igatha.ui.component.Section\nimport com.nizarmah.igatha.ui.component.SectionItem\nimport java.util.Date\nimport java.util.Locale\nimport java.util.UUID\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun DeviceDetailView(\n    device: Device,\n    onBackClick: () -> Unit\n) {\n    val timeSinceLastSeen = remember(device.lastSeen) {\n        calculateTimeSinceLastSeen(device.lastSeen)\n    }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(\"Device Details\") },\n                navigationIcon = {\n                    IconButton(onClick = onBackClick) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Sharp.ArrowBack,\n                            contentDescription = \"Go back\"\n                        )\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainer\n                )\n            )\n        }\n    ) { paddingValues ->\n        Column(\n            modifier = Modifier\n                .padding(paddingValues)\n                .fillMaxSize()\n                .background(MaterialTheme.colorScheme.surfaceContainer)\n        ) {\n            LazyColumn {\n                item {\n                    Section(\n                        header = \"Identity\",\n                        footer = \"Identity is pseudonymized for privacy.\"\n                    ) {\n                        SectionItem {\n                            Text(text = \"Name\", style = MaterialTheme.typography.bodyMedium)\n\n                            Spacer(modifier = Modifier.padding(4.dp))\n\n                            Text(\n                                text = device.shortName,\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        }\n\n                        HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))\n\n                        SectionItem {\n                            Text(text = \"ID\", style = MaterialTheme.typography.bodyMedium)\n\n                            Spacer(modifier = Modifier.padding(4.dp))\n\n                            Text(\n                                text = device.id,\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.secondary,\n                                fontFamily = FontFamily.Monospace,\n                                textAlign = TextAlign.End,\n                                modifier = Modifier.fillMaxWidth(),\n                                letterSpacing = (-0.5).sp\n                            )\n                        }\n                    }\n                }\n                item {\n                    Section(\n                        header = \"Location\",\n                        footer = \"Location is limited by used tech. Direction is not available. Distance is approximate and varies due to signal fluctuations. It is for general guidance only.\"\n                    ) {\n                        SectionItem {\n                            Text(text = \"Distance\", style = MaterialTheme.typography.bodyMedium)\n\n                            Spacer(modifier = Modifier.padding(4.dp))\n\n                            Text(\n                                text = String.format(\n                                    Locale.getDefault(),\n                                    \"%.1f meters away\",\n                                    device.estimateDistance()\n                                ),\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                fontFamily = FontFamily.Monospace\n                            )\n                        }\n                    }\n                }\n                item {\n                    Section(\n                        header = \"Status\",\n                        footer = \"Status shows if the device is active and in range.\"\n                    ) {\n                        SectionItem {\n                            Text(text = \"Last Seen\", style = MaterialTheme.typography.bodyMedium)\n\n                            Spacer(modifier = Modifier.padding(4.dp))\n\n                            Text(\n                                text = timeSinceLastSeen,\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nfun calculateTimeSinceLastSeen(lastSeen: Date): String {\n    val now = Date()\n    val diff = now.time - lastSeen.time\n    val seconds = diff / 1000\n    val minutes = seconds / 60\n    val hours = minutes / 60\n    val days = hours / 24\n    val weeks = days / 7\n\n    return when {\n        seconds < 60 -> \"$seconds second${if (seconds != 1L) \"s\" else \"\"} ago\"\n        minutes < 60 -> \"$minutes minute${if (minutes != 1L) \"s\" else \"\"} ago\"\n        hours < 24 -> \"$hours hour${if (hours != 1L) \"s\" else \"\"} ago\"\n        days < 7 -> \"$days day${if (days != 1L) \"s\" else \"\"} ago\"\n        else -> \"$weeks week${if (weeks != 1L) \"s\" else \"\"} ago\"\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nfun DeviceDetailViewPreview() {\n    IgathaTheme {\n        DeviceDetailView(\n            device = Device(\n                id = UUID.randomUUID(),\n                rssi = -40.0\n            ),\n            onBackClick = {\n                // do nothing\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/view/DeviceListView.kt",
    "content": "package com.nizarmah.igatha.ui.view\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.nizarmah.igatha.model.Device\nimport com.nizarmah.igatha.ui.component.Section\nimport com.nizarmah.igatha.ui.theme.IgathaTheme\nimport java.util.Date\nimport java.util.UUID\n\n@Composable\nfun DeviceListView(devices: List<Device>, onDeviceClick: (Device) -> Unit) {\n    LazyColumn(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(MaterialTheme.colorScheme.surfaceContainer)\n    ) {\n        item {\n            Section (\n                header = \"People seeking help\",\n                footer = \"Note: Distance is approximate and varies due to signal fluctuations. It is for general guidance only. Proximity scanning requires location on Android 11 or lower.\"\n            ) {\n                if (devices.isEmpty()) {\n                    Text(\n                        \"No devices found nearby.\",\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(16.dp)\n                    )\n                } else {\n                    devices.forEachIndexed { index, device ->\n                        DeviceRowView(\n                            device = device,\n                            onClick = { onDeviceClick(device) }\n                        )\n\n                        if (index < devices.size - 1) {\n                            HorizontalDivider(\n                                modifier = Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = 16.dp),\n                                color = MaterialTheme.colorScheme.outlineVariant\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\n@Preview(showBackground = true)\nfun DeviceListViewPreview() {\n    val mockDevices = listOf(\n        Device(id = UUID.randomUUID(), rssi = -40.0),\n        Device(id = UUID.randomUUID(), rssi = -60.0, lastSeen = Date(System.currentTimeMillis() - 600_000)),\n        Device(id = UUID.randomUUID(), rssi = -75.0),\n        Device(id = UUID.randomUUID(), rssi = -85.0)\n    )\n\n    IgathaTheme {\n        DeviceListView(devices = mockDevices) {}\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/view/DeviceRowView.kt",
    "content": "package com.nizarmah.igatha.ui.view\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.sharp.KeyboardArrowRight\nimport androidx.compose.material.icons.filled.Person\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.nizarmah.igatha.model.Device\nimport com.nizarmah.igatha.ui.theme.IgathaTheme\nimport java.util.*\n\n@Composable\nfun DeviceRowView(device: Device, onClick: () -> Unit = {}) {\n    val isStale = device.lastSeen.before(\n        Date(System.currentTimeMillis() - 300_000)\n    ) // 5 minutes ago\n\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable { onClick() }\n            .padding(16.dp)\n            .alpha(if (isStale) 0.4f else 1.0f),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Icon(\n            imageVector = Icons.Default.Person,\n            contentDescription = \"Device Icon\",\n            tint = MaterialTheme.colorScheme.onSecondary,\n            modifier = Modifier\n                .size(40.dp)\n                .background(\n                    color = MaterialTheme.colorScheme.secondary,\n                    shape = CircleShape\n                )\n                .padding(5.dp)\n        )\n        Spacer(modifier = Modifier.width(16.dp))\n        Column(\n            modifier = Modifier.weight(1f)\n        ) {\n            Text(\n                text = device.shortName,\n                style = MaterialTheme.typography.titleMedium,\n                color = MaterialTheme.colorScheme.onSurface\n            )\n\n            Spacer(modifier = Modifier.height(4.dp))\n\n            Text(\n                text = String.format(\n                    Locale.getDefault(),\n                    \"%.1f meters away\",\n                    device.estimateDistance()),\n                style = MaterialTheme.typography.bodySmall,\n                fontFamily = FontFamily.Monospace,\n                color = MaterialTheme.colorScheme.onSurfaceVariant\n            )\n        }\n        Icon(\n            imageVector = Icons.AutoMirrored.Sharp.KeyboardArrowRight,\n            contentDescription = \"See device details\",\n            tint = MaterialTheme.colorScheme.onSurfaceVariant\n        )\n    }\n}\n\n@Composable\n@Preview(showBackground = true)\nfun DeviceRowViewPreview() {\n    val previewDevice = Device(\n        id = UUID.randomUUID(),\n        rssi = -60.0\n    )\n\n    IgathaTheme {\n        DeviceRowView(device = previewDevice)\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/view/FeedbackFormView.kt",
    "content": "package com.nizarmah.igatha.ui.view\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.sharp.ArrowBack\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusDirection\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardCapitalization\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.nizarmah.igatha.ui.component.Section\nimport com.nizarmah.igatha.ui.theme.IgathaTheme\nimport com.nizarmah.igatha.viewmodel.FormState\nimport com.nizarmah.igatha.viewmodel.UsageReason\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)\n@Composable\nfun FeedbackFormView(\n    formState: FormState,\n    customUsage: String,\n    ideas: String,\n    email: String,\n    hasCustomUsage: Boolean,\n    isUsageReasonSelected: (UsageReason) -> Boolean,\n    onUsageReasonToggle: (UsageReason) -> Unit,\n    onCustomUsageChange: (String) -> Unit,\n    onIdeasChange: (String) -> Unit,\n    onEmailChange: (String) -> Unit,\n    onSubmit: () -> Unit,\n    onBackClick: () -> Unit\n) {\n    // Focus and keyboard management\n    val focusManager = LocalFocusManager.current\n    val keyboardController = LocalSoftwareKeyboardController.current\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(\"Share Feedback\") },\n                navigationIcon = {\n                    IconButton(onClick = onBackClick) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Sharp.ArrowBack,\n                            contentDescription = \"Go back\"\n                        )\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainer\n                )\n            )\n        }\n    ) { paddingValues ->\n        Column(\n            modifier = Modifier\n                .padding(paddingValues)\n                .fillMaxSize()\n                .background(MaterialTheme.colorScheme.surfaceContainer)\n        ) {\n            LazyColumn {\n                // Usage reasons section\n                item {\n                    Section(\n                        header = \"Why do you use Igatha?\",\n                        footer = \"Select all that apply.\"\n                    ) {\n                        Column {\n                            // Usage reason items\n                            UsageReason.allCases.forEachIndexed { index, reason ->\n                                UsageReasonItem(\n                                    reason = reason,\n                                    isSelected = isUsageReasonSelected(reason),\n                                    onToggle = { onUsageReasonToggle(reason) }\n                                )\n\n                                // Add divider between items (except for the last one)\n                                if (index < UsageReason.allCases.size - 1) {\n                                    HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant)\n                                }\n                            }\n\n                            // Custom reason text field (conditional)\n                            if (hasCustomUsage) {\n                                HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant)\n                                TextField(\n                                    value = customUsage,\n                                    onValueChange = onCustomUsageChange,\n                                    modifier = Modifier.fillMaxWidth(),\n                                    placeholder = { Text(\"Please describe\") },\n                                    colors = TextFieldDefaults.colors(\n                                        unfocusedContainerColor = MaterialTheme.colorScheme.surface,\n                                        focusedContainerColor = MaterialTheme.colorScheme.surface,\n                                        unfocusedIndicatorColor = MaterialTheme.colorScheme.surfaceVariant,\n                                        focusedIndicatorColor = MaterialTheme.colorScheme.primary\n                                    ),\n                                    singleLine = true,\n                                    keyboardOptions = KeyboardOptions(\n                                        capitalization = KeyboardCapitalization.Sentences,\n                                        imeAction = ImeAction.Next\n                                    ),\n                                    keyboardActions = KeyboardActions(\n                                        onNext = { focusManager.moveFocus(FocusDirection.Down) }\n                                    )\n                                )\n                            }\n                        }\n                    }\n                }\n\n                // Ideas section\n                item {\n                    Section(\n                        header = \"What would make Igatha more helpful?\",\n                        footer = \"Optional. If you have any ideas, don't hesitate.\"\n                    ) {\n                        TextField(\n                            value = ideas,\n                            onValueChange = onIdeasChange,\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .height(120.dp),\n                            colors = TextFieldDefaults.colors(\n                                unfocusedContainerColor = MaterialTheme.colorScheme.surface,\n                                focusedContainerColor = MaterialTheme.colorScheme.surface,\n                                unfocusedIndicatorColor = MaterialTheme.colorScheme.surfaceVariant,\n                                focusedIndicatorColor = MaterialTheme.colorScheme.primary\n                            ),\n                            keyboardOptions = KeyboardOptions(\n                                capitalization = KeyboardCapitalization.Sentences,\n                                imeAction = ImeAction.Next\n                            ),\n                            keyboardActions = KeyboardActions(\n                                onNext = { focusManager.moveFocus(FocusDirection.Down) }\n                            )\n                        )\n                    }\n                }\n\n                // Email section\n                item {\n                    Section(\n                        header = \"Your email\",\n                        footer = \"Optional. We'll only contact you for clarifications.\"\n                    ) {\n                        TextField(\n                            value = email,\n                            onValueChange = onEmailChange,\n                            modifier = Modifier.fillMaxWidth(),\n                            placeholder = { Text(\"Email address\") },\n                            colors = TextFieldDefaults.colors(\n                                unfocusedContainerColor = MaterialTheme.colorScheme.surface,\n                                focusedContainerColor = MaterialTheme.colorScheme.surface,\n                                unfocusedIndicatorColor = MaterialTheme.colorScheme.surfaceVariant,\n                                focusedIndicatorColor = MaterialTheme.colorScheme.primary\n                            ),\n                            keyboardOptions = KeyboardOptions(\n                                keyboardType = KeyboardType.Email,\n                                capitalization = KeyboardCapitalization.None,\n                                imeAction = ImeAction.Done\n                            ),\n                            keyboardActions = KeyboardActions(\n                                onDone = {\n                                    keyboardController?.hide()\n                                    focusManager.clearFocus()\n                                }\n                            ),\n                            singleLine = true\n                        )\n                    }\n                }\n\n                // Submit button section\n                item {\n                    Section(\n                        padding = Modifier.padding(vertical = 16.dp)\n                    ) {\n                        Button(\n                            onClick = {\n                                keyboardController?.hide()\n                                focusManager.clearFocus()\n                                onSubmit()\n                            },\n                            modifier = Modifier\n                                .fillMaxWidth(),\n                            enabled = formState == FormState.Idle,\n                            shape = RoundedCornerShape(8.dp),\n                            colors = ButtonDefaults.buttonColors(\n                                containerColor = MaterialTheme.colorScheme.surface,\n                                contentColor = MaterialTheme.colorScheme.primary\n                            ),\n                            contentPadding = PaddingValues(16.dp)\n                        ) {\n                            Row(\n                                horizontalArrangement = Arrangement.SpaceBetween,\n                                verticalAlignment = Alignment.CenterVertically,\n                                modifier = Modifier.fillMaxWidth()\n                            ) {\n                                Text(\n                                    text = \"Submit\",\n                                    style = MaterialTheme.typography.bodyLarge\n                                )\n\n                                // Show progress indicator if submitting\n                                if (formState == FormState.Submitting) {\n                                    CircularProgressIndicator(\n                                        modifier = Modifier.size(20.dp),\n                                        strokeWidth = 2.dp\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // Spacer\n                item {\n                    Spacer(modifier = Modifier.height(16.dp))\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun UsageReasonItem(\n    reason: UsageReason,\n    isSelected: Boolean,\n    onToggle: () -> Unit\n) {\n    // State that tracks the visual selection state, initially set to the actual selection state\n    val (visuallySelected, setVisuallySelected) = androidx.compose.runtime.remember(isSelected) {\n        androidx.compose.runtime.mutableStateOf(isSelected)\n    }\n\n    // Update the visual state when the actual selection state changes\n    androidx.compose.runtime.LaunchedEffect(isSelected) {\n        setVisuallySelected(isSelected)\n    }\n\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable {\n                // Immediately update visual state for responsive feedback\n                setVisuallySelected(!visuallySelected)\n                // Then trigger the actual model update\n                onToggle()\n            }\n            .padding(vertical = 12.dp, horizontal = 16.dp),\n        horizontalArrangement = Arrangement.SpaceBetween,\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        // Reason content\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = reason.displayString,\n                style = MaterialTheme.typography.bodyLarge\n            )\n\n            // Add spacing between title and example\n            Spacer(modifier = Modifier.height(4.dp))\n\n            // Subtext\n            reason.exampleString?.let { example ->\n                Text(\n                    text = example,\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    lineHeight = MaterialTheme.typography.bodySmall.fontSize.times(1.4f)\n                )\n            }\n        }\n\n        // Checkmark for selected reasons - use visuallySelected for immediate feedback\n        if (visuallySelected) {\n            Icon(\n                imageVector = Icons.Default.Check,\n                contentDescription = \"Selected\",\n                tint = MaterialTheme.colorScheme.primary\n            )\n        }\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nfun FeedbackFormViewPreview() {\n    IgathaTheme {\n        FeedbackFormView(\n            formState = FormState.Idle,\n            customUsage = \"I use it for my personal safety\",\n            ideas = \"Would be nice to have offline maps\",\n            email = \"user@example.com\",\n            hasCustomUsage = true,\n            isUsageReasonSelected = { it in setOf(UsageReason.DisasterPreparedness, UsageReason.Other) },\n            onUsageReasonToggle = {},\n            onCustomUsageChange = {},\n            onIdeasChange = {},\n            onEmailChange = {},\n            onSubmit = {},\n            onBackClick = {}\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/ui/view/SettingsView.kt",
    "content": "package com.nizarmah.igatha.ui.view\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.sharp.ArrowBack\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.nizarmah.igatha.ui.component.FeedbackButtonView\nimport com.nizarmah.igatha.ui.component.Section\nimport com.nizarmah.igatha.ui.component.SectionItem\nimport com.nizarmah.igatha.ui.theme.IgathaTheme\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SettingsView(\n    disasterDetectionEnabled: Boolean,\n    isDisasterDetectionAvailable: Boolean,\n    onDisasterDetectionEnabledChanged: (Boolean) -> Unit,\n    onBackClick: () -> Unit,\n    onFeedbackClick: () -> Unit = {}\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(\"Settings\") },\n                navigationIcon = {\n                    IconButton(onClick = onBackClick) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Sharp.ArrowBack,\n                            contentDescription = \"Go back\"\n                        )\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainer\n                )\n            )\n        }\n    ) { paddingValues ->\n        Column(\n            modifier = Modifier\n                .padding(paddingValues)\n                .fillMaxSize()\n                .background(MaterialTheme.colorScheme.surfaceContainer)\n        ) {\n            LazyColumn {\n                item {\n                    Section(\n                        header = \"Background Services\",\n                        footer = \"Services might require additional permissions.\"\n                    ) {\n                        SectionItem {\n                            Column(modifier = Modifier.weight(1f)) {\n                                Text(\n                                    text = \"Disaster Detection\",\n                                    style = MaterialTheme.typography.bodyLarge\n                                )\n\n                                Spacer(modifier = Modifier.height(4.dp))\n\n                                Text(\n                                    text = \"Detects disasters and sends SOS when the app is not in use. This may increase battery consumption.\",\n                                    style = MaterialTheme.typography.bodySmall,\n                                    color = MaterialTheme.colorScheme.secondary,\n                                    lineHeight = MaterialTheme.typography.bodySmall.fontSize.times(1.4f)\n                                )\n                            }\n\n                            Spacer(modifier = Modifier.padding(2.dp))\n\n                            Switch(\n                                checked = disasterDetectionEnabled,\n                                onCheckedChange = onDisasterDetectionEnabledChanged,\n                                enabled = isDisasterDetectionAvailable\n                            )\n                        }\n                    }\n                }\n\n                item {\n                    Section(\n                        header = \"Feedback\",\n                        footer = \"Your feedback helps us improve Igatha, for everyone.\"\n                    ) {\n                        Box(modifier = Modifier.padding(horizontal = 0.dp)) {\n                            FeedbackButtonView(\n                                onClick = onFeedbackClick\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nfun SettingsViewPreview() {\n    IgathaTheme {\n        SettingsView(\n            disasterDetectionEnabled = true,\n            isDisasterDetectionAvailable = true,\n            onDisasterDetectionEnabledChanged = {},\n            onBackClick = {}\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/util/PermissionsHelper.kt",
    "content": "package com.nizarmah.igatha.util\n\nimport android.Manifest\nimport android.os.Build\nimport kotlin.collections.plus\n\nobject PermissionsHelper {\n    fun getNotificationsPermissions(): Array<String> {\n        var permissions = emptyArray<String>()\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            permissions += arrayOf(\n                Manifest.permission.POST_NOTIFICATIONS\n            )\n        }\n\n        return permissions\n    }\n\n    fun getSOSPermissions(): Array<String> {\n        var permissions = arrayOf(\n            // SOS Beacon\n            Manifest.permission.BLUETOOTH,\n            Manifest.permission.BLUETOOTH_ADMIN,\n            // Siren Player\n            Manifest.permission.MODIFY_AUDIO_SETTINGS\n        )\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n            permissions += arrayOf(\n                // SOS Beacon\n                Manifest.permission.BLUETOOTH_ADVERTISE\n            )\n        }\n\n        permissions += getServicePermissions()\n\n        return permissions\n    }\n\n    fun getProximityScanPermissions(): Array<String> {\n        var permissions = arrayOf(\n            // ProximityScanner\n            Manifest.permission.BLUETOOTH,\n            Manifest.permission.BLUETOOTH_ADMIN,\n        )\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n            permissions += arrayOf(\n                // ProximityScanner\n                Manifest.permission.BLUETOOTH_SCAN,\n                Manifest.permission.BLUETOOTH_CONNECT,\n            )\n        }\n\n        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {\n            permissions += arrayOf(\n                // ProximityScanner\n                Manifest.permission.ACCESS_COARSE_LOCATION\n            )\n        }\n\n        return permissions\n    }\n\n    fun getDisasterDetectionPermissions(): Array<String> {\n        var permissions = emptyArray<String>()\n\n        permissions += getServicePermissions()\n\n        return permissions\n    }\n\n    private fun getServicePermissions(): Array<String> {\n        var permissions = emptyArray<String>()\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n            permissions += arrayOf(\n                Manifest.permission.FOREGROUND_SERVICE\n            )\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n            permissions += arrayOf(\n                Manifest.permission.HIGH_SAMPLING_RATE_SENSORS\n            )\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n            permissions += arrayOf(\n                Manifest.permission.FOREGROUND_SERVICE_HEALTH\n            )\n        }\n\n        // Foreground services need a notification\n        // So, check notification permissions as well\n        permissions += getNotificationsPermissions()\n\n        return permissions\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/util/PermissionsManager.kt",
    "content": "package com.nizarmah.igatha.util\n\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport androidx.core.content.ContextCompat\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.stateIn\n\nobject PermissionsManager {\n    private val _notificationsPermitted = MutableStateFlow(false)\n    val notificationsPermitted: StateFlow<Boolean> = _notificationsPermitted.asStateFlow()\n\n    private val _sosPermitted = MutableStateFlow(false)\n    val sosPermitted: StateFlow<Boolean> = _sosPermitted.asStateFlow()\n\n    private val _disasterDetectionPermitted = MutableStateFlow(false)\n    val disasterDetectionPermitted: StateFlow<Boolean> = _disasterDetectionPermitted.asStateFlow()\n\n    private val _proximityScanPermitted = MutableStateFlow(false)\n    val proximityScanPermitted: StateFlow<Boolean> = _proximityScanPermitted.asStateFlow()\n\n    @OptIn(DelicateCoroutinesApi::class)\n    val permissionsGranted: StateFlow<Boolean> = combine(\n        sosPermitted,\n        disasterDetectionPermitted,\n        proximityScanPermitted,\n        notificationsPermitted\n    ) { sos, disaster, proximity, notifications ->\n        sos && disaster && proximity && notifications\n    }.stateIn(GlobalScope, SharingStarted.Eagerly, false)\n\n    // Initialize by checking current permissions\n    fun init(context: Context) {\n        refreshPermissions(context)\n    }\n\n    // Function to refresh the current permission state\n    fun refreshPermissions(context: Context) {\n        _notificationsPermitted.value = hasPermissions(context, PermissionsHelper.getNotificationsPermissions())\n        _sosPermitted.value = hasPermissions(context, PermissionsHelper.getSOSPermissions())\n        _proximityScanPermitted.value = hasPermissions(context, PermissionsHelper.getProximityScanPermissions())\n        _disasterDetectionPermitted.value = hasPermissions(context, PermissionsHelper.getDisasterDetectionPermissions())\n    }\n\n    // Helper function to check permissions\n    private fun hasPermissions(context: Context, permissions: Array<String>): Boolean {\n        return permissions.all { permission ->\n            ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/util/SettingsManager.kt",
    "content": "package com.nizarmah.igatha.util\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport com.nizarmah.igatha.Constants\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\n\nobject SettingsManager {\n    private val _disasterDetectionEnabled = MutableStateFlow(true)\n    val disasterDetectionEnabled: StateFlow<Boolean> = _disasterDetectionEnabled.asStateFlow()\n\n    private lateinit var sharedPreferences: SharedPreferences\n\n    fun init(context: Context) {\n        sharedPreferences = getSharedPreferences(context)\n\n        // Initialize the StateFlow with the current value\n        val enabled = sharedPreferences.getBoolean(Constants.DISASTER_DETECTION_ENABLED_KEY, true)\n        _disasterDetectionEnabled.value = enabled\n\n        // Listen for changes in SharedPreferences\n        sharedPreferences.registerOnSharedPreferenceChangeListener { prefs, key ->\n            if (key == Constants.DISASTER_DETECTION_ENABLED_KEY) {\n                val newValue = prefs.getBoolean(key, true)\n                _disasterDetectionEnabled.value = newValue\n            }\n        }\n    }\n\n    fun setDisasterDetectionEnabled(enabled: Boolean) {\n        _disasterDetectionEnabled.value = enabled\n\n        sharedPreferences.edit().putBoolean(\n            Constants.DISASTER_DETECTION_ENABLED_KEY, enabled).apply()\n    }\n\n    private fun getSharedPreferences(context: Context): SharedPreferences {\n        return context.getSharedPreferences(\n            Constants.SHARED_PREFERENCES_KEY,\n            Context.MODE_PRIVATE\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/viewmodel/ContentViewModel.kt",
    "content": "package com.nizarmah.igatha.viewmodel\n\nimport android.app.Application\nimport android.content.Intent\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.nizarmah.igatha.Constants\nimport com.nizarmah.igatha.model.Device\nimport com.nizarmah.igatha.service.DisasterDetectionService\nimport com.nizarmah.igatha.service.EmergencyManager\nimport com.nizarmah.igatha.service.ProximityScanner\nimport com.nizarmah.igatha.service.SOSService\nimport com.nizarmah.igatha.util.PermissionsManager\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\n\nclass ContentViewModel(app: Application) : AndroidViewModel(app) {\n\n    private val emergencyManager = EmergencyManager.getInstance(getApplication())\n    private val proximityScanner = ProximityScanner(getApplication())\n\n    // SOS state\n    val isSOSAvailable: StateFlow<Boolean> = emergencyManager.isSOSAvailable\n    val isSOSActive: StateFlow<Boolean> = emergencyManager.isSOSActive\n\n    // ProximityScanner state\n    val isProximityScanAvailable: StateFlow<Boolean> = combine(\n        proximityScanner.isAvailable,\n        PermissionsManager.proximityScanPermitted\n    ) { isAvailable, isPermitted ->\n        isAvailable && isPermitted\n    }.stateIn(viewModelScope, SharingStarted.Eagerly, false)\n\n    // Active alert state\n    private val _activeAlert = MutableStateFlow<AlertType?>(null)\n    val activeAlert: StateFlow<AlertType?> = _activeAlert.asStateFlow()\n\n    private val _devicesMap = MutableStateFlow<Map<String, Device>>(emptyMap())\n    val devices: StateFlow<List<Device>> = _devicesMap.asStateFlow()\n        .map { devicesMap ->\n            devicesMap.values.sortedByDescending { it.rssi }\n        }\n        .stateIn(\n            viewModelScope,\n            SharingStarted.Eagerly,\n            emptyList()\n        )\n\n    init {\n        // Observe the disaster detection availability\n        viewModelScope.launch {\n            emergencyManager.isDetectorEnabled.collect { available ->\n                if (available) {\n                    startDisasterDetectionService()\n                } else {\n                    stopDisasterDetectionService()\n                }\n            }\n        }\n\n        // Observe proximity scanner's scanned devices\n        viewModelScope.launch {\n            proximityScanner.scannedDevices.collect { device ->\n                device?.let { updateDevice(it) }\n            }\n        }\n\n        // Start or stop the scanner based on availability\n        viewModelScope.launch {\n            isProximityScanAvailable.collect { available ->\n                if (available) {\n                    proximityScanner.startScanning()\n                } else {\n                    proximityScanner.stopScanning()\n                }\n            }\n        }\n    }\n\n    private fun startDisasterDetectionService() {\n        val context = getApplication<Application>()\n        val serviceIntent = Intent(context, DisasterDetectionService::class.java)\n        context.startForegroundService(serviceIntent)\n    }\n\n    private fun stopDisasterDetectionService() {\n        val context = getApplication<Application>()\n        val serviceIntent = Intent(context, DisasterDetectionService::class.java)\n        context.stopService(serviceIntent)\n    }\n\n    private fun startSOSService() {\n        val context = getApplication<Application>()\n        val serviceIntent = Intent(context, SOSService::class.java)\n        context.startForegroundService(serviceIntent)\n    }\n\n    private fun stopSOSService() {\n        val context = getApplication<Application>()\n        val serviceIntent = Intent(context, SOSService::class.java).apply {\n            action = Constants.ACTION_STOP_SOS\n        }\n        context.startService(serviceIntent)\n    }\n\n    fun startSOS() {\n        if (!isSOSAvailable.value || isSOSActive.value) {\n            return\n        }\n\n        startSOSService()\n    }\n\n    fun stopSOS() {\n        stopSOSService()\n    }\n\n    fun showSOSConfirmation() {\n        if (!isSOSAvailable.value || isSOSActive.value) {\n            return\n        }\n\n        _activeAlert.value = AlertType.SOSConfirmation\n    }\n\n    fun dismissAlert() {\n        _activeAlert.value = null\n    }\n\n    private fun updateDevice(device: Device) {\n        _devicesMap.update { currentMap ->\n            val updatedMap = currentMap.toMutableMap()\n            updatedMap[device.id] = device\n            updatedMap\n        }\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        proximityScanner.deinit()\n    }\n}\n\nsealed class AlertType(id: Int) {\n    object SOSConfirmation : AlertType(1)\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/viewmodel/FeedbackFormViewModel.kt",
    "content": "package com.nizarmah.igatha.viewmodel\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.CoroutineDispatcher\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.json.JSONObject\nimport java.net.HttpURLConnection\nimport java.net.URL\nimport java.net.URLEncoder\nimport java.util.UUID\n\n/**\n * FeedbackFormViewModel handles all logic for the feedback form view.\n */\nclass FeedbackFormViewModel(\n    app: Application,\n    private val feedbackService: FeedbackService = UsageFeedbackGoogleForm(Dispatchers.IO)\n) : AndroidViewModel(app) {\n\n    // Form state\n    private val _formState = MutableStateFlow<FormState>(FormState.Idle)\n    val formState: StateFlow<FormState> = _formState.asStateFlow()\n\n    // Submission result\n    private val _submissionResult = MutableStateFlow<SubmissionResult?>(null)\n    val submissionResult: StateFlow<SubmissionResult?> = _submissionResult.asStateFlow()\n\n    // Form data\n    private val _usageReasons = MutableStateFlow<Set<UsageReason>>(emptySet())\n\n    // Form bindings\n    private val _customUsage = MutableStateFlow(\"\")\n    val customUsage: StateFlow<String> = _customUsage.asStateFlow()\n\n    private val _ideas = MutableStateFlow(\"\")\n    val ideas: StateFlow<String> = _ideas.asStateFlow()\n\n    private val _email = MutableStateFlow(\"\")\n    val email: StateFlow<String> = _email.asStateFlow()\n\n    // Checks if the user selected the \"other\" usage reason\n    val hasCustomUsage: StateFlow<Boolean> = MutableStateFlow(false).apply {\n        viewModelScope.launch {\n            _usageReasons.collect { reasons ->\n                value = reasons.contains(UsageReason.Other)\n            }\n        }\n    }\n\n    // Updates the form data\n    fun updateCustomUsage(value: String) {\n        _customUsage.value = value\n    }\n\n    fun updateIdeas(value: String) {\n        _ideas.value = value\n    }\n\n    fun updateEmail(value: String) {\n        _email.value = value\n    }\n\n    // Checks if the usage reason is selected\n    fun isUsageReasonSelected(reason: UsageReason): Boolean {\n        return _usageReasons.value.contains(reason)\n    }\n\n    // Toggles the usage reason\n    fun toggleUsageReason(reason: UsageReason) {\n        val currentReasons = _usageReasons.value.toMutableSet()\n        if (currentReasons.contains(reason)) {\n            currentReasons.remove(reason)\n        } else {\n            currentReasons.add(reason)\n        }\n        _usageReasons.value = currentReasons\n    }\n\n    // Dismisses submission result alert\n    fun dismissAlert() {\n        _submissionResult.value = null\n    }\n\n    // Validates the form and returns the error if it's invalid\n    fun validateForm(): String? {\n        // Form is valid if at least one usage reason is selected\n        if (_usageReasons.value.isEmpty()) {\n            return \"Please select at least one usage reason.\"\n        }\n\n        // If \"other\" is selected, custom usage should not be empty\n        if (_usageReasons.value.contains(UsageReason.Other) &&\n            _customUsage.value.trim().isEmpty()) {\n            return \"Please specify why you chose 'other'.\"\n        }\n\n        return null\n    }\n\n    // Submits the feedback form\n    fun submit() {\n        // Validate the form and return the error if it's invalid\n        val errorMessage = validateForm()\n        if (errorMessage != null) {\n            _submissionResult.value = SubmissionResult.Error(errorMessage)\n            return\n        }\n\n        // Update form state to submitting\n        _formState.value = FormState.Submitting\n\n        // Create a feedback object\n        val feedback = Feedback(\n            usageReasons = _usageReasons.value,\n            customUsage = _customUsage.value,\n            ideas = _ideas.value,\n            email = _email.value\n        )\n\n        // Submit the form asynchronously\n        viewModelScope.launch {\n            try {\n                // Try to submit the form\n                feedbackService.submit(feedback)\n\n                // Show the success alert\n                _submissionResult.value = SubmissionResult.Success\n            } catch (e: Exception) {\n                // Show the error alert with a more descriptive message\n                val errorMessage = when {\n                    e.message.isNullOrBlank() -> \"Connection error. Check your internet connection.\"\n                    else -> e.message\n                }\n\n                // Generate a truncated reference ID\n                val refId = UUID.randomUUID().toString().substring(0, 8)\n\n                // Log the error with full details for troubleshooting (shows in logcat)\n                // TODO: Replace local logging with an error reporting service\n                android.util.Log.e(\n                    \"FeedbackForm\",\n                    \"Error submitting form - Ref: $refId - Details: $errorMessage\",\n                    e\n                )\n\n                _submissionResult.value = SubmissionResult.Error(\n                    \"Your feedback could not be submitted. Please try again later. (Ref: $refId)\"\n                )\n            } finally {\n                // Update form state to idle\n                _formState.value = FormState.Idle\n            }\n        }\n    }\n}\n\n/**\n * Interface for feedback services to abstract the submission process.\n */\ninterface FeedbackService {\n    suspend fun submit(feedback: Feedback)\n}\n\n/**\n * UsageFeedbackGoogleForm has the submission logic for https://forms.gle/rcu3MZjPYww7Fbnh7.\n * This class performs network operations in a coroutine context.\n */\nclass UsageFeedbackGoogleForm(private val ioDispatcher: CoroutineDispatcher) : FeedbackService {\n    // Form URL for the POST request\n    private val formUrl = \"https://docs.google.com/forms/u/0/d/e/1FAIpQLSdCdNYIaPcg2-eAs1Mlvwoa6P5Ijqfdb1hmWlaA-poIKpMDtQ/formResponse\"\n\n    // Feedback field identifier\n    private val feedbackField = \"entry.457989095\"\n\n    // Submits the feedback form\n    override suspend fun submit(feedback: Feedback) = withContext(ioDispatcher) {\n        // Create a URL connection\n        val url = URL(formUrl)\n        val connection = url.openConnection() as HttpURLConnection\n\n        try {\n            // Set up the connection\n            connection.requestMethod = \"POST\"\n            connection.doOutput = true\n            connection.setRequestProperty(\"Content-Type\", \"application/x-www-form-urlencoded\")\n            connection.connectTimeout = 15000 // 15 seconds timeout\n            connection.readTimeout = 15000 // 15 seconds timeout\n\n            // Create the form data\n            val postData = \"${feedbackField}=${URLEncoder.encode(feedback.toJson(), \"UTF-8\")}\"\n\n            // Send the request\n            connection.outputStream.use { os ->\n                val input = postData.toByteArray(Charsets.UTF_8)\n                os.write(input, 0, input.size)\n            }\n\n            // Check the response code\n            val responseCode = connection.responseCode\n            if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_MOVED_TEMP) {\n                throw Exception(\"HTTP error: $responseCode\")\n            }\n        } catch (e: Exception) {\n            throw Exception(\"Network error: ${e.message ?: e.javaClass.simpleName}\")\n        } finally {\n            connection.disconnect()\n        }\n    }\n}\n\n/**\n * Feedback stores the form data and converts it to a JSON string.\n */\ndata class Feedback(\n    val usageReasons: Set<UsageReason>,\n    val customUsage: String,\n    val ideas: String,\n    val email: String\n) {\n    // Constant form data\n    private val device = \"android\"\n    private val key = \"android-usage-feedback-v1.0.0\"\n\n    // Converts the form data to a JSON string\n    fun toJson(): String {\n        val jsonObject = JSONObject().apply {\n            put(\"usage\", JSONObject().apply {\n                put(\"reasons\", usageReasons.map { it.displayString })\n                put(\"custom\", customUsage)\n            })\n            put(\"ideas\", ideas)\n            put(\"email\", email)\n            put(\"device\", device)\n            put(\"key\", key)\n        }\n\n        return jsonObject.toString()\n    }\n}\n\n/**\n * FormState stores the state of the form.\n */\nsealed class FormState {\n    object Idle : FormState()\n    object Submitting : FormState()\n}\n\n/**\n * SubmissionResult stores the result of the form submission.\n */\nsealed class SubmissionResult {\n    object Success : SubmissionResult()\n    data class Error(val message: String) : SubmissionResult()\n}\n\n/**\n * UsageReason stores all usage reasons and their examples.\n */\nenum class UsageReason {\n    DisasterPreparedness,\n    AdventureTravel,\n    Caregiving,\n    WorkplaceSafety,\n    RegionalConflict,\n    Other;\n\n    // Display string is the primary text for the usage reason\n    val displayString: String\n        get() = when (this) {\n            DisasterPreparedness -> \"Disaster Preparedness\"\n            AdventureTravel -> \"Adventure & Travel\"\n            Caregiving -> \"Caregiving\"\n            WorkplaceSafety -> \"Workplace Safety\"\n            RegionalConflict -> \"Regional Conflict or Instability\"\n            Other -> \"Other\"\n        }\n\n    // Example string is the subtext for the usage reason\n    val exampleString: String?\n        get() = when (this) {\n            DisasterPreparedness -> \"e.g., earthquakes, floods, conflicts\"\n            AdventureTravel -> \"e.g., hiking, biking, remote trips\"\n            Caregiving -> \"e.g., monitoring elderly or dependents\"\n            WorkplaceSafety -> \"e.g., construction, mining, field jobs\"\n            RegionalConflict -> \"e.g., war, civil unrest, political instability\"\n            Other -> \"Please specify below\"\n        }\n\n    companion object {\n        // All cases for iteration\n        val allCases: List<UsageReason> = entries.toList()\n    }\n}\n\n/* Notes\n\n [1]:\n    I am aware that the Google form can be spammed.\n    I care most about emails, and this helps me reach users directly.\n    I made sure there's no long term or financial damage from spam.\n    This cheap implementation to get emails here outweighs any other.\n\n */\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/viewmodel/FeedbackFormViewModelFactory.kt",
    "content": "package com.nizarmah.igatha.viewmodel\n\nimport android.app.Application\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\n\n/**\n * Factory for creating a FeedbackFormViewModel with a constructor that takes an application.\n */\nclass FeedbackFormViewModelFactory(private val application: Application) : ViewModelProvider.Factory {\n    @Suppress(\"UNCHECKED_CAST\")\n    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n        if (modelClass.isAssignableFrom(FeedbackFormViewModel::class.java)) {\n            return FeedbackFormViewModel(application) as T\n        }\n        throw IllegalArgumentException(\"Unknown ViewModel class\")\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/viewmodel/SettingsViewModel.kt",
    "content": "package com.nizarmah.igatha.viewmodel\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidViewModel\nimport com.nizarmah.igatha.util.SettingsManager\nimport kotlinx.coroutines.flow.StateFlow\nimport com.nizarmah.igatha.service.EmergencyManager\n\nclass SettingsViewModel(app: Application) : AndroidViewModel(app) {\n    val disasterDetectionEnabled: StateFlow<Boolean> = SettingsManager.disasterDetectionEnabled\n    val isDisasterDetectionAvailable: StateFlow<Boolean> = EmergencyManager.getInstance(getApplication()).isDetectorAvailable\n\n    fun setDisasterDetectionEnabled(enabled: Boolean) {\n        SettingsManager.setDisasterDetectionEnabled(enabled)\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/java/com/nizarmah/igatha/viewmodel/SettingsViewModelFactory.kt",
    "content": "package com.nizarmah.igatha.viewmodel\n\nimport android.app.Application\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\n\nclass SettingsViewModelFactory(private val application: Application) : ViewModelProvider.Factory {\n    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n        if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) {\n            @Suppress(\"UNCHECKED_CAST\")\n            return SettingsViewModel(application) as T\n        }\n        throw IllegalArgumentException(\"Unknown ViewModel class\")\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_im_okay.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z\"/>\n    \n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#3DDC84\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"#FFFFFF\">\n  <group android:scaleX=\"0.522\"\n      android:scaleY=\"0.522\"\n      android:translateX=\"5.736\"\n      android:translateY=\"5.736\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M20.79,9.23l-2,-3.46l-4.79,2.77l0,-5.54l-4,0l0,5.54l-4.79,-2.77l-2,3.46l4.79,2.77l-4.79,2.77l2,3.46l4.79,-2.77l0,5.54l4,0l0,-5.54l4.79,2.77l2,-3.46l-4.79,-2.77z\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_need_help.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M13.5,7h-3c-1.1,0 -2,0.9 -2,2v6c0,1.1 0.9,2 2,2h3c1.1,0 2,-0.9 2,-2V9C15.5,7.9 14.6,7 13.5,7zM13.5,15h-3V9h3V15zM1,15h4v-2H3c-1.1,0 -2,-0.9 -2,-2V9c0,-1.1 0.9,-2 2,-2h4v2H3v2h2c1.1,0 2,0.9 2,2v2c0,1.1 -0.9,2 -2,2H1V15zM17,15h4v-2h-2c-1.1,0 -2,-0.9 -2,-2V9c0,-1.1 0.9,-2 2,-2h4v2h-4v2h2c1.1,0 2,0.9 2,2v2c0,1.1 -0.9,2 -2,2h-4V15z\"/>\n    \n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_notification.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M20.79,9.23l-2,-3.46l-4.79,2.77l0,-5.54l-4,0l0,5.54l-4.79,-2.77l-2,3.46l4.79,2.77l-4.79,2.77l2,3.46l4.79,-2.77l0,5.54l4,0l0,-5.54l4.79,2.77l2,-3.46l-4.79,-2.77z\"/>\n    \n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_notification_alerting.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M15.73,3L8.27,3L3,8.27v7.46L8.27,21h7.46L21,15.73L21,8.27L15.73,3zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3 0,-0.72 0.58,-1.3 1.3,-1.3 0.72,0 1.3,0.58 1.3,1.3 0,0.72 -0.58,1.3 -1.3,1.3zM13,13h-2L11,7h2v6z\"/>\n    \n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_notification_signaling.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,9c-3.15,0 -6,2.41 -6,6.15c0,2.49 2,5.44 6,8.85c4,-3.41 6,-6.36 6,-8.85C18,11.41 15.15,9 12,9zM12,16.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5S12.83,16.5 12,16.5zM12,4c1.93,0 3.68,0.78 4.95,2.05l-1.41,1.41C14.63,6.56 13.38,6 12,6S9.37,6.56 8.46,7.46L7.05,6.05C8.32,4.78 10.07,4 12,4zM19.78,3.23l-1.41,1.41C16.74,3.01 14.49,2 12.01,2S7.27,3.01 5.64,4.63L4.22,3.22C6.22,1.23 8.97,0 12.01,0S17.79,1.23 19.78,3.23z\"/>\n    \n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_stop_sos.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM16,16H8V8h8V16z\"/>\n    \n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purple_500\">#FF6200EE</color>\n    <color name=\"purple_700\">#FF3700B3</color>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_700\">#FF018786</color>\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#FF3B30</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">Igatha</string>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/themes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.Igatha\" parent=\"android:Theme.Material.Light.NoActionBar\" />\n</resources>"
  },
  {
    "path": "android/app/src/main/res/xml/backup_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample backup rules file; uncomment and customize as necessary.\n   See https://developer.android.com/guide/topics/data/autobackup\n   for details.\n   Note: This file is ignored for devices older that API 31\n   See https://developer.android.com/about/versions/12/backup-restore\n-->\n<full-backup-content>\n    <!--\n   <include domain=\"sharedpref\" path=\".\"/>\n   <exclude domain=\"sharedpref\" path=\"device.xml\"/>\n-->\n</full-backup-content>"
  },
  {
    "path": "android/app/src/main/res/xml/data_extraction_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample data extraction rules file; uncomment and customize as necessary.\n   See https://developer.android.com/about/versions/12/backup-restore#xml-changes\n   for details.\n-->\n<data-extraction-rules>\n    <cloud-backup>\n        <!-- TODO: Use <include> and <exclude> to control what is backed up.\n        <include .../>\n        <exclude .../>\n        -->\n    </cloud-backup>\n    <!--\n    <device-transfer>\n        <include .../>\n        <exclude .../>\n    </device-transfer>\n    -->\n</data-extraction-rules>"
  },
  {
    "path": "android/app/src/test/java/com/nizarmah/igatha/ExampleUnitTest.kt",
    "content": "package com.nizarmah.igatha\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  },
  {
    "path": "android/build.gradle.kts",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nplugins {\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.kotlin.android) apply false\n    alias(libs.plugins.kotlin.compose) apply false\n}"
  },
  {
    "path": "android/gradle/libs.versions.toml",
    "content": "[versions]\nagp = \"8.9.2\"\ngson = \"2.10.1\"\nkotlin = \"2.0.0\"\ncoreKtx = \"1.16.0\"\njunit = \"4.13.2\"\njunitVersion = \"1.2.1\"\nespressoCore = \"3.6.1\"\nlifecycleRuntimeKtx = \"2.8.7\"\nactivityCompose = \"1.10.1\"\nnavigationCompose = \"2.8.9\"\ncomposeBom = \"2025.04.01\"\nworkRuntimeKtx = \"2.10.1\"\n\n[libraries]\nandroidx-core-ktx = { group = \"androidx.core\", name = \"core-ktx\", version.ref = \"coreKtx\" }\nandroidx-navigation-compose = { module = \"androidx.navigation:navigation-compose\", version.ref = \"navigationCompose\" }\ngson = { module = \"com.google.code.gson:gson\", version.ref = \"gson\" }\njunit = { group = \"junit\", name = \"junit\", version.ref = \"junit\" }\nandroidx-junit = { group = \"androidx.test.ext\", name = \"junit\", version.ref = \"junitVersion\" }\nandroidx-espresso-core = { group = \"androidx.test.espresso\", name = \"espresso-core\", version.ref = \"espressoCore\" }\nandroidx-lifecycle-runtime-ktx = { group = \"androidx.lifecycle\", name = \"lifecycle-runtime-ktx\", version.ref = \"lifecycleRuntimeKtx\" }\nandroidx-activity-compose = { group = \"androidx.activity\", name = \"activity-compose\", version.ref = \"activityCompose\" }\nandroidx-compose-bom = { group = \"androidx.compose\", name = \"compose-bom\", version.ref = \"composeBom\" }\nandroidx-ui = { group = \"androidx.compose.ui\", name = \"ui\" }\nandroidx-ui-graphics = { group = \"androidx.compose.ui\", name = \"ui-graphics\" }\nandroidx-ui-tooling = { group = \"androidx.compose.ui\", name = \"ui-tooling\" }\nandroidx-ui-tooling-preview = { group = \"androidx.compose.ui\", name = \"ui-tooling-preview\" }\nandroidx-ui-test-manifest = { group = \"androidx.compose.ui\", name = \"ui-test-manifest\" }\nandroidx-ui-test-junit4 = { group = \"androidx.compose.ui\", name = \"ui-test-junit4\" }\nandroidx-material3 = { group = \"androidx.compose.material3\", name = \"material3\" }\nandroidx-work-runtime-ktx = { group = \"androidx.work\", name = \"work-runtime-ktx\", version.ref = \"workRuntimeKtx\" }\n\n[plugins]\nandroid-application = { id = \"com.android.application\", version.ref = \"agp\" }\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nkotlin-compose = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\n\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.13-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. For more details, visit\n# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\n# Enables namespacing of each library's R class so that its R class includes only the\n# resources declared in the library itself and none from the library's dependencies,\n# thereby reducing the size of the R class for that library\nandroid.nonTransitiveRClass=true"
  },
  {
    "path": "android/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "android/gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "android/settings.gradle.kts",
    "content": "pluginManagement {\n    repositories {\n        google {\n            content {\n                includeGroupByRegex(\"com\\\\.android.*\")\n                includeGroupByRegex(\"com\\\\.google.*\")\n                includeGroupByRegex(\"androidx.*\")\n            }\n        }\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.name = \"Igatha\"\ninclude(\":app\")\n"
  },
  {
    "path": "fastlane/metadata/android/de/full_description.txt",
    "content": "<p><i>Igatha</i> ist eine Offline-SOS-App für Notfälle, wenn Kommunikationsnetzwerke ausfallen.</p><p><i>Igatha:</i> Eine Lebensader, wenn Sie sie am meisten brauchen</p><p>In Krisenzeiten, wenn herkömmliche Kommunikationsnetze nicht verfügbar sind, bietet <i>Igatha</i> eine Möglichkeit, mithilfe der Bluetooth-Technologie Hilfe zu signalisieren. <i>Igatha</i> wurde für Situationen wie Kriegsgebiete, Naturkatastrophen oder abgelegene Orte entwickelt und funktioniert vollständig offline, damit Sie mit anderen in der Nähe in Kontakt treten können.</p><p><br><b>Merkmale:</b></p><ul><li>Offline-SOS-Übertragung: Senden Sie ein SOS-Signal über Bluetooth Low Energy (BLE), ohne dass eine Internet- oder Mobilfunkverbindung erforderlich ist.</li><li>Signalerkennung in der Nähe: Suchen Sie nach SOS-Signalen von anderen in Ihrer Nähe, die möglicherweise ebenfalls Hilfe benötigen.</li><li>Ungefähre Entfernungsschätzung: Machen Sie sich ein Bild davon, wie nah andere Personen sind, die SOS-Signale gesendet oder empfangen haben.</li><li>Automatische Notfallerkennung: Die App überwacht bestimmte Sensordaten, um mögliche Notfälle wie plötzliche Bewegungen zu erkennen, und kann automatisch ein SOS-Signal senden.</li></ul><p><b>Wichtige Informationen:</b></p><ul><li>Datenschutz respektvoll: Igatha erhebt oder speichert keine personenbezogenen Daten. Alle Vorgänge werden lokal auf Ihrem Gerät ausgeführt.</li><li>Open Source: Der Quellcode der App ist unter github.com/nizarmah/igatha verfügbar. Sie können es gerne überprüfen, beisteuern oder bei Bedarf ändern.</li><li>Einschränkungen: Diese App ist eine frühe Version (MVP) und funktioniert möglicherweise nicht in allen Szenarien perfekt. Die Tests waren begrenzt. Es handelt sich nicht um eine garantierte Rettungsmethode, sie kann jedoch hilfreich sein, wenn keine anderen Optionen verfügbar sind.</li></ul><p><b>Für wen es ist:</b></p><ul><li>Personen in Gebieten mit beeinträchtigter Kommunikationsinfrastruktur.</li><li>Menschen in Konfliktgebieten oder bei Naturkatastrophen.</li><li>Jeder, der möglicherweise keinen Zugang zu herkömmlichen Kommunikationsnetzen hat.</li></ul><p><b>Haftungsausschluss:</b></p><ul><li>Igatha soll in Notfällen helfen, sollte aber andere Sicherheitsmaßnahmen nicht ersetzen. Nutzen Sie immer alle verfügbaren Ressourcen, um Ihre Sicherheit zu gewährleisten.</li></ul>"
  },
  {
    "path": "fastlane/metadata/android/de/short_description.txt",
    "content": "SOS-Signalisierung und -Wiederherstellung"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "<p><i>Igatha</i> is an offline SOS app designed for emergencies when communication networks fail.</p><p><i>Igatha:</i> A Lifeline When You Need It Most</p><p>In times of crisis, when traditional communication networks are unavailable, Igatha offers a way to signal for help using Bluetooth technology. Designed for situations like war zones, natural disasters, or remote locations, Igatha operates entirely offline to help you connect with others nearby.</p><p><br><b>Features:</b></p><ul><li>Offline SOS Broadcasting: Send out an SOS signal via Bluetooth Low Energy (BLE) without needing internet or cellular service.</li><li>Nearby Signal Detection: Scan for SOS signals from others in your vicinity who may also need assistance.</li><li>Approximate Distance Estimation: Get an idea of how close others are who have sent or received SOS signals.</li><li>Automatic Emergency Detection: The app monitors certain sensor data to detect possible emergencies, such as sudden movements, and can automatically send an SOS signal.</li></ul><p><b>Important Information:</b></p><ul><li>Privacy Respectful: Igatha does not collect or store any personal data. All operations are performed locally on your device.</li><li>Open Source: The app’s source code is available at github.com/nizarmah/igatha. You are welcome to review, contribute, or modify it as needed.</li><li>Limitations: This app is an early version (MVP) and may not function perfectly in all scenarios. Testing has been limited. It is not a guaranteed method of rescue but may provide assistance when no other options are available.</li></ul><p><b>Who It’s For:</b></p><ul><li>Individuals in areas with compromised communication inrastructure.</li><li>People in conflict zones or experiencing natural disasters.</li><li>Anyone who might find themselves without access to traditional communication networks.</li></ul><p><b>Disclaimer:</b></p><ul><li>Igatha is intended to assist in emergencies but should not replace other safety measures. Always use all available resources to ensure your safety.</li></ul>"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "SOS Signaling &amp; Recovery"
  },
  {
    "path": "ios/Igatha/AppDelegate.swift",
    "content": "//\n//  AppDelegate.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 14/10/2024.\n//\n\nimport SwiftUI\nimport UserNotifications\n\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n    var emergencyManager: EmergencyManager!\n    var notificationManager: NotificationManager!\n    var deepLinkHandler: DeepLinkHandler!\n\n    func application(\n        _ application: UIApplication,\n        didFinishLaunchingWithOptions launchOptions:\n        [UIApplication.LaunchOptionsKey: Any]?\n    ) -> Bool {\n        // Initialize managers in the background\n        Task { await setupManagers() }\n\n        return true\n    }\n\n    private func setupManagers() async {\n        // Initialize notification manager first since it needs to be set as delegate\n        notificationManager = NotificationManager.shared\n\n        // Initialize emergency manager\n        emergencyManager = EmergencyManager.shared\n\n        // Initialize deep link handler\n        deepLinkHandler = DeepLinkHandler.shared\n\n        // Set up notification manager (will request permissions and schedule notifications)\n        await notificationManager.setup()\n    }\n\n    // Handle deep links when app is launched via URL\n    func application(\n        _ app: UIApplication,\n        open url: URL,\n        options: [UIApplication.OpenURLOptionsKey : Any] = [:]\n    ) -> Bool {\n        return deepLinkHandler.handleDeepLink(url)\n    }\n\n    // Handle deep links in universal links format\n    func application(\n        _ application: UIApplication,\n        continue userActivity: NSUserActivity,\n        restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void\n    ) -> Bool {\n        if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL {\n            return deepLinkHandler.handleDeepLink(url)\n        }\n\n        return false\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Assets.xcassets/AccentColor.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"platform\" : \"universal\",\n        \"reference\" : \"systemRedColor\"\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Igatha/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"AppIcon-20@2x.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"AppIcon-20@3x.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"AppIcon-29@2x.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"AppIcon-29@3x.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"AppIcon~ipad.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"38x38\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"38x38\"\n    },\n    {\n      \"filename\" : \"AppIcon-40@2x.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"AppIcon-40@3x.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"AppIcon@2x.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"filename\" : \"AppIcon@3x.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"64x64\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"64x64\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"68x68\"\n    },\n    {\n      \"filename\" : \"AppIcon@2x~ipad 1.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"76x76\"\n    },\n    {\n      \"filename\" : \"AppIcon-83.5@2x~ipad.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"83.5x83.5\"\n    },\n    {\n      \"filename\" : \"AppIcon~ios-marketing.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"38x38\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"38x38\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"64x64\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"64x64\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"68x68\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"76x76\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"83.5x83.5\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"38x38\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"38x38\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"64x64\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"3x\",\n      \"size\" : \"64x64\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"68x68\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"76x76\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"scale\" : \"2x\",\n      \"size\" : \"83.5x83.5\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Igatha/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Igatha/Constants.swift",
    "content": "//\n//  Constants.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 06/10/2024.\n//\n\nimport CoreBluetooth\n\nstruct Constants {\n    static let SOSBeaconServiceID: CBUUID = CBUUID(string: \"1802\")\n    static let SOSBeaconRestoreID: String = \"com.nizarmah.igatha.sosbeacon\"\n\n    // threshold for sudden changes in linear acceleration\n    // 3.0 g ~= dropping your phone on a hard surface\n    static let SensorAccelerationThreshold: Double = 3.0\n    // threshold for sudden changes in rotation\n    // 6.0 r/s ~= almost a full rotation in 1 second\n    static let SensorRotationThreshold: Double = 6.0\n    // threshold for sudden changes in atmospheric pressure\n    // 0.1 kPa ~= altitude change of approx. 8 to 12 meters\n    static let SensorPressureThreshold: Double = 0.1\n\n    // key for disaster detector enabled setting in app storage\n    static let DisasterDetectionSettingsKey: String = \"disasterDetectionEnabled\"\n    // time window for temporally correlating sensor readings\n    // if all thresholds exceed in 1.5s then we have an incident\n    static let DisasterTemporalCorrelationTimeWindow: Double = 1.5\n    // grace period (seconds) before an incident response is triggered\n    static let DisasterResponseGracePeriod: Double = 120.0\n    static let DisasterResponseNotificationID: String = \"DISASTER_RESPONSE\"\n\n    // percentage of how much a new value should affect the old value\n    static let RSSIExponentialMovingAverageSmoothingFactor: Double = 0.18\n\n    // Deep links.\n    struct DeepLink {\n        // Properties.\n        static let Key: String = \"deepLink\"\n        static let Scheme: String = \"igatha\"\n\n        // Actions.\n        struct Settings {\n            static let Name: String = \"OpenSettings\"\n            static let Value: String = \"settings\"\n        }\n    }\n\n    // Notification identifiers\n    struct Notifications {\n        struct Feedback {\n            static let Id: String = \"feedbackRequest\"\n\n            // We open Settings instead of FeedbackForm because it's easier\n            // Chaining NavigationLink(isActive) is a bit difficult, when nested\n            // Once we start using iOS 16+, we'll be able to open the form directly\n            static let Link = DeepLink.Settings.self\n\n            // Delay before the notification is shown (in seconds)\n            static let TriggerDelay: TimeInterval = 3 * 24 * 60 * 60\n\n            // Key for the timestamp of the feedback request notification\n            static let TimestampKey: String = \"feedbackScheduledTimestamp\"\n        }\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Igatha.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.developer.sustained-execution</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Igatha/IgathaApp.swift",
    "content": "//\n//  IgathaApp.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 05/10/2024.\n//\n\nimport SwiftUI\n\n@main\nstruct IgathaApp: App {\n    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate\n    \n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n        }\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <!-- Bluetooh Usage Descriptions -->\n        <key>NSBluetoothAlwaysUsageDescription</key>\n        <string>Used to find SOS signals during emergencies.</string>\n        <key>NSBluetoothPeripheralUsageDescription</key>\n        <string>Used to signal SOS during emergencies.</string>\n\n        <!-- Motion Usage Descriptions -->\n        <key>NSMotionUsageDescription</key>\n        <string>Used to detect disasters which affected the user.</string>\n\n        <!-- Location Usage Descriptions -->\n        <key>NSLocationWhenInUseUsageDescription</key>\n        <string>Used to enable the sensors for disaster detection in the foreground.</string>\n        <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>\n        <string>Used to enable the sensors for disaster detection in the background.</string>\n\n        <!-- Background modes -->\n        <key>UIBackgroundModes</key>\n        <array>\n            <!-- Used by the SOS beacon -->\n            <string>bluetooth-peripheral</string>\n            <!-- Used by the siren player -->\n            <string>audio</string>\n            <!-- Used by disaster detector -->\n            <string>location</string>\n            <!-- Used for scheduled notifications -->\n            <string>remote-notification</string>\n        </array>\n\n        <!-- URL Scheme for Deep Links -->\n        <key>CFBundleURLTypes</key>\n        <array>\n            <dict>\n                <key>CFBundleTypeRole</key>\n                <string>Editor</string>\n                <key>CFBundleURLName</key>\n                <string>com.nizarmah.igatha</string>\n                <key>CFBundleURLSchemes</key>\n                <array>\n                    <string>igatha</string>\n                </array>\n            </dict>\n        </array>\n\n        <!-- Compliance -->\n        <key>ITSAppUsesNonExemptEncryption</key>\n        <false/>\n        <key>LSMinimumSystemVersion</key>\n        <string>13.0.0</string>\n    </dict>\n</plist>\n"
  },
  {
    "path": "ios/Igatha/Models/Device.swift",
    "content": "//\n//  Models.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 06/10/2024.\n//\n\nimport Foundation\nimport CoreBluetooth\n\nextension UUID {\n    // returns the first 8 chars from a uuid string\n    var shortName: String {\n        return self.uuidString.prefix(8).uppercased()\n    }\n}\n\nclass Device: Identifiable, ObservableObject {\n    let id: String\n    let shortName: String\n    \n    @Published var rssi: Double\n    @Published var lastSeen: Date\n    \n    init(\n        id: UUID,\n        rssi: Double,\n        lastSeen: Date = Date()\n    ) {\n        self.id = id.uuidString\n        self.shortName = id.shortName\n        \n        self.rssi = rssi\n        self.lastSeen = lastSeen\n    }\n    \n    func update(\n        rssi: Double,\n        lastSeen: Date = Date()\n    ) {\n        let oldRSSI = self.rssi\n        let newRSSI = rssi\n        \n        // smoothing factor\n        let alpha = Constants.RSSIExponentialMovingAverageSmoothingFactor\n        \n        // smoothen the RSSI with exponential moving average\n        let smoothedRSSI = alpha * newRSSI + (1 - alpha) * oldRSSI\n        \n        self.rssi = smoothedRSSI\n        self.lastSeen = lastSeen\n    }\n    \n    func estimateDistance(\n        using pathLossExponent: PathLossExponent = .urban\n    ) -> Double {\n        // 1 meter ~= -59.0 RSSI\n        let txPower = -59.0\n        \n        // path-loss exponent\n        let n: Double = pathLossExponent.value\n        \n        let distance = pow(10.0, (txPower - rssi) / (10.0 * n))\n        \n        // round for simplicity simplicity\n        return (distance * 1000.0).rounded() / 1000.0\n    }\n}\n\nenum PathLossExponent {\n    // path-loss exponent (n)\n    \n    // free open spaces\n    // n = 2.0\n    case freeSpace\n    \n    // indoor environments\n    // n = 3.0\n    case indoor\n    \n    // dense urban environments\n    // n = 4.0\n    case urban\n    \n    var value: Double {\n        switch self {\n        case .freeSpace:\n            return 2.0\n        case .indoor:\n            return 3.0\n        case .urban:\n            return 4.0\n        }\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Preview Content/Preview Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Igatha/Sensors/AccelerometerSensor.swift",
    "content": "//\n//  AccelerometerSensor.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 12/10/2024.\n//\n\nimport Foundation\nimport CoreMotion\n\nclass AccelerometerSensor: Sensor {\n    weak var delegate: SensorDelegate?\n    \n    typealias T = CMMotionManager\n    internal let sensor: CMMotionManager\n    \n    internal let threshold: Double\n    private let updateInterval: TimeInterval\n    \n    public var isAvailable: Bool {\n        return sensor.isAccelerometerAvailable\n    }\n    \n    init(\n        threshold: Double,\n        updateInterval: TimeInterval\n    ) {\n        self.threshold = threshold\n        self.updateInterval = updateInterval\n        \n        sensor = CMMotionManager()\n        sensor.accelerometerUpdateInterval = threshold\n    }\n    \n    func startUpdates() {\n        guard isAvailable else { return }\n        \n        sensor.startAccelerometerUpdates(\n            to: .main\n        ) { [weak self] data, error in\n            guard\n                let self = self,\n                let data = data\n            else { return }\n            \n            let acceleration = data.acceleration\n            \n            let totalAcceleration = sqrt(\n                acceleration.x * acceleration.x +\n                acceleration.y * acceleration.y +\n                acceleration.z * acceleration.z\n            )\n            \n            guard totalAcceleration > self.threshold else { return }\n            \n            self.delegate?.sensorExceededThreshold(\n                sensorType: .accelerometer,\n                eventTime: Date()\n            )\n        }\n        \n        NSLog(\"AccelerometerSensor: started updates\")\n    }\n    \n    func stopUpdates() {\n        sensor.stopAccelerometerUpdates()\n        \n        NSLog(\"AccelerometerSensor: stopped updates\")\n    }\n}\n\n"
  },
  {
    "path": "ios/Igatha/Sensors/BarometerSensor.swift",
    "content": "//\n//  BarometerSensor.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 12/10/2024.\n//\n\nimport Foundation\nimport CoreMotion\n\nclass BarometerSensor: Sensor {\n    weak var delegate: SensorDelegate?\n    \n    typealias T = CMAltimeter\n    internal let sensor: CMAltimeter\n    \n    internal let threshold: Double    \n    private var initialPressure: Double?\n    \n    public var isAvailable: Bool {\n        return CMAltimeter.isRelativeAltitudeAvailable()\n    }\n    \n    init(\n        threshold: Double\n    ) {\n        self.threshold = threshold\n        \n        sensor = CMAltimeter()\n    }\n    \n    func startUpdates() {\n        guard isAvailable else { return }\n        \n        sensor.startRelativeAltitudeUpdates(\n            to: .main\n        ) { [weak self] data, error in\n            guard\n                let self = self,\n                let data = data\n            else { return }\n            \n            let pressure = data.pressure.doubleValue // in kPa\n            if self.initialPressure == nil {\n                self.initialPressure = pressure\n                return\n            }\n            \n            guard let initialPressure = self.initialPressure else { return }\n            \n            let pressureChange = abs(pressure - initialPressure)\n            \n            guard pressureChange > self.threshold else { return }\n            \n            self.delegate?.sensorExceededThreshold(\n                sensorType: .barometer,\n                eventTime: Date()\n            )\n        }\n        \n        NSLog(\"BarometerSensor: started updates\")\n    }\n    \n    func stopUpdates() {\n        sensor.stopRelativeAltitudeUpdates()\n        \n        NSLog(\"BarometerSensor: stopped updates\")\n    }\n}\n\n"
  },
  {
    "path": "ios/Igatha/Sensors/GyroscopeSensor.swift",
    "content": "//\n//  GyroscopeSensor.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 12/10/2024.\n//\n\nimport Foundation\nimport CoreMotion\n\nclass GyroscopeSensor: Sensor {\n    weak var delegate: SensorDelegate?\n    \n    typealias T = CMMotionManager\n    internal let sensor: CMMotionManager\n    \n    internal let threshold: Double\n    private let updateInterval: TimeInterval\n    \n    public var isAvailable: Bool {\n        return sensor.isGyroAvailable\n    }\n    \n    init(\n        threshold: Double,\n        updateInterval: TimeInterval\n    ) {\n        self.threshold = threshold\n        self.updateInterval = updateInterval\n        \n        sensor = CMMotionManager()\n        sensor.gyroUpdateInterval = updateInterval\n    }\n    \n    func startUpdates() {\n        guard isAvailable else { return }\n        \n        sensor.startGyroUpdates(\n            to: .main\n        ) { [weak self] data, error in\n            guard\n                let self = self,\n                let data = data\n            else { return }\n            \n            let rotationRate = data.rotationRate\n            \n            let totalRotationRate = sqrt(\n                rotationRate.x * rotationRate.x +\n                rotationRate.y * rotationRate.y +\n                rotationRate.z * rotationRate.z\n            )\n            \n            guard totalRotationRate > self.threshold else { return }\n            \n            self.delegate?.sensorExceededThreshold(\n                sensorType: .gyroscope,\n                eventTime: Date()\n            )\n        }\n        \n        NSLog(\"GyroscopeSensor: started updates\")\n    }\n    \n    func stopUpdates() {\n        sensor.stopGyroUpdates()\n        \n        NSLog(\"GyroscopeSensor: stopped updates\")\n    }\n}\n\n"
  },
  {
    "path": "ios/Igatha/Sensors/SensorType.swift",
    "content": "//\n//  SensorType.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 12/10/2024.\n//\n\nimport Foundation\n\nenum SensorType {\n    case accelerometer\n    case gyroscope\n    case barometer\n}\n\npublic protocol AnySensor: AnyObject {\n    var isAvailable: Bool { get }\n    \n    func startUpdates()\n    func stopUpdates()\n}\n\nprotocol Sensor: AnySensor {\n    var delegate: SensorDelegate? { get }\n    \n    associatedtype T\n    var sensor: T { get }\n    \n    var threshold: Double { get }\n}\n\nprotocol SensorDelegate: AnyObject {\n    func sensorExceededThreshold(sensorType: SensorType, eventTime: Date)\n}\n"
  },
  {
    "path": "ios/Igatha/Services/DeepLinkHandler.swift",
    "content": "//\n//  DeepLinkHandler.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 22/04/2025.\n//\n\nimport Foundation\nimport SwiftUI\n\nclass DeepLinkHandler: ObservableObject {\n    static let shared = DeepLinkHandler()\n\n    @Published var showSettings = false\n\n    private init() {\n        // Listen for deep link events\n\n        registerSettingsObserver()\n    }\n\n    // registerSettingsObserver listens for settings deep link notification events\n    private func registerSettingsObserver() {\n        NotificationCenter.default.addObserver(\n            self,\n            selector: #selector(handleSettings),\n            name: NSNotification.Name(Constants.DeepLink.Settings.Name),\n            object: nil\n        )\n    }\n\n    // handleSettings opens the settings view\n    @objc @MainActor private func handleSettings() {\n        showSettings = true\n    }\n\n    // handleDeepLink is called from the outside to handle a deep link\n    @MainActor func handleDeepLink(_ url: URL) -> Bool {\n        // Check if the URL scheme is igatha\n        guard let scheme = url.scheme, scheme == Constants.DeepLink.Scheme else {\n            return false\n        }\n\n        // Check if the host is feedback\n        if url.host == Constants.DeepLink.Settings.Value {\n            handleSettings()\n            return true\n        }\n\n        return false\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Services/DisasterDetector.swift",
    "content": "//\n//  DisasterDetector.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 12/10/2024.\n//\n\nimport Foundation\n\nclass DisasterDetector {\n    weak var delegate: DisasterDetectorDelegate?\n    \n    private let eventTimeWindow: TimeInterval\n    private var eventTimes: [SensorType: Date] = [:]\n    \n    private let locationManager: LocationManager\n    \n    private let accelerometerSensor: AccelerometerSensor\n    private let gyroscopeSensor: GyroscopeSensor\n    private let barometerSensor: BarometerSensor\n    \n    public var isAvailable: Bool {\n        let isEnabled = UserDefaults.standard.bool(\n            forKey: Constants.DisasterDetectionSettingsKey\n        )\n        \n        return (\n            isEnabled &&\n            locationManager.isAvailable &&\n            accelerometerSensor.isAvailable &&\n            gyroscopeSensor.isAvailable &&\n            barometerSensor.isAvailable\n        )\n    }\n    public var isActive: Bool {\n        return locationManager.isActive\n    }\n    \n    init(\n        accelerationThreshold: Double,\n        rotationThreshold: Double,\n        pressureThreshold: Double,\n        eventTimeWindow: TimeInterval\n    ) {\n        self.eventTimeWindow = eventTimeWindow\n        \n        locationManager = LocationManager()\n        \n        accelerometerSensor = AccelerometerSensor(\n            threshold: accelerationThreshold,\n            updateInterval: 0.1\n        )\n        gyroscopeSensor = GyroscopeSensor(\n            threshold: rotationThreshold,\n            updateInterval: 0.1\n        )\n        barometerSensor = BarometerSensor(\n            threshold: pressureThreshold\n        )\n        \n        locationManager.delegate = self\n        \n        accelerometerSensor.delegate = self\n        gyroscopeSensor.delegate = self\n        barometerSensor.delegate = self\n        \n        delegate?.disasterDetectorAvailabilityUpdate(isAvailable)\n    }\n    \n    deinit {\n        stopDetection()\n    }\n    \n    func startDetection() {\n        guard\n            isAvailable\n                && !isActive\n        else { return }\n        \n        locationManager.startUpdates()\n        \n        NSLog(\"DisasterDetector: started detection\")\n    }\n    \n    func stopDetection() {\n        locationManager.stopUpdates()\n        \n        NSLog(\"DisasterDetector: stopped detection\")\n    }\n}\n\nextension DisasterDetector: LocationManagerDelegate {\n    func locationUpdatesStarted() {\n        guard\n            isAvailable\n                && !isActive\n        else { return }\n        \n        accelerometerSensor.startUpdates()\n        gyroscopeSensor.startUpdates()\n        barometerSensor.startUpdates()\n        \n        delegate?.disasterDetectionStarted()\n    }\n    \n    func locationUpdatesStopped() {\n        accelerometerSensor.stopUpdates()\n        gyroscopeSensor.stopUpdates()\n        barometerSensor.stopUpdates()\n        \n        delegate?.disasterDetectionStopped()\n    }\n    \n    func locationManagerAvailabilityUpdate(_ isAvailable: Bool) {\n        delegate?.disasterDetectorAvailabilityUpdate(self.isAvailable)\n        \n        if !isAvailable {\n            stopDetection()\n        }\n    }\n}\n\nextension DisasterDetector: SensorDelegate {\n    // called when a sensor exceeds a threshold\n    func sensorExceededThreshold(\n        sensorType: SensorType,\n        eventTime: Date\n    ) {\n        eventTimes[sensorType] = eventTime\n        \n        checkForIncident()\n        \n        NSLog(\"DisasterDetector: \\(sensorType) exceeded threshold\")\n    }\n    \n    // called when an incident is suspected\n    private func checkForIncident() {\n        let currentTime = Date()\n        \n        // ensure all events have occurred\n        guard eventTimes.count == 3 else { return }\n        \n        // ensure all events occurred within the time window\n        for (_, eventTime) in eventTimes {\n            if currentTime.timeIntervalSince(eventTime) > eventTimeWindow {\n                // event is too old; do not consider it\n                return\n            }\n        }\n        \n        NSLog(\"DisasterDetector: incident detected\")\n        \n        // incident detected\n        delegate?.disasterDetected()\n    }\n}\n\nprotocol DisasterDetectorDelegate: AnyObject {\n    func disasterDetected()\n    \n    func disasterDetectionStarted()\n    func disasterDetectionStopped()\n    \n    func disasterDetectorAvailabilityUpdate(_ isAvailable: Bool)\n}\n"
  },
  {
    "path": "ios/Igatha/Services/EmergencyManager.swift",
    "content": "//\n//  EmergencyManager.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 14/10/2024.\n//\n\nimport SwiftUI\nimport UserNotifications\n\nclass EmergencyManager: NSObject {\n    static let shared = EmergencyManager()\n    \n    weak var delegate: EmergencyManagerDelegate?\n    \n    private var disasterDetector: DisasterDetector!\n    \n    public var isDetectorAvailable: Bool {\n        return disasterDetector.isAvailable\n    }\n    public var isDetectorActive: Bool {\n        return disasterDetector.isActive\n    }\n    \n    private var sosBeacon: SOSBeacon!\n    private var sirenPlayer: SirenPlayer!\n    \n    public var isSOSAvailable: Bool {\n        return (\n            sosBeacon.isAvailable\n            && sirenPlayer.isAvailable\n        )\n    }\n    public var isSOSActive: Bool {\n        return (\n            sosBeacon.isActive\n            || sirenPlayer.isActive\n        )\n    }\n    \n    private var confirmationTimer: Timer?\n    private let confirmationGracePeriod: TimeInterval = Constants.DisasterResponseGracePeriod\n    \n    private override init() {\n        super.init()\n        \n        sosBeacon = SOSBeacon()\n        sirenPlayer = SirenPlayer()\n        \n        disasterDetector = DisasterDetector(\n            accelerationThreshold: Constants.SensorAccelerationThreshold,\n            rotationThreshold: Constants.SensorRotationThreshold,\n            pressureThreshold: Constants.SensorPressureThreshold,\n            eventTimeWindow: Constants.DisasterTemporalCorrelationTimeWindow\n        )\n        \n        sirenPlayer.delegate = self\n        sosBeacon.delegate = self\n        \n        disasterDetector.delegate = self\n        \n        // setup local notifications\n        setupNotificationCategories()\n        UNUserNotificationCenter.current().delegate = self\n        requestNotificationPermissions()\n    }\n    \n    deinit {\n        stopSOS()\n        \n        disasterDetector.stopDetection()\n    }\n    \n    func startDetector() {\n        guard\n            isDetectorAvailable\n                && !isDetectorActive\n        else { return }\n        \n        disasterDetector.startDetection()\n        \n        delegate?.detectorStarted()\n    }\n    \n    func stopDetector() {\n        disasterDetector.stopDetection()\n        \n        delegate?.detectorStopped()\n    }\n    \n    @objc func startSOS() {\n        guard\n            isSOSAvailable\n                && !isSOSActive\n        else { return }\n        \n        sosBeacon.startBroadcasting()\n        sirenPlayer.startSiren()\n        \n        delegate?.sosStarted()\n    }\n    \n    func stopSOS() {\n        sirenPlayer.stopSiren()\n        sosBeacon.stopBroadcasting()\n        \n        confirmationTimer?.invalidate()\n        confirmationTimer = nil\n        \n        delegate?.sosStopped()\n    }\n    \n    // starts confirmation timer for SOS\n    func startConfirmationTimer() {\n        confirmationTimer?.invalidate()\n        \n        // Schedule a new timer\n        confirmationTimer = Timer.scheduledTimer(\n            timeInterval: confirmationGracePeriod,\n            target: self,\n            selector: #selector(startSOS),\n            userInfo: nil,\n            repeats: false\n        )\n    }\n}\n\nextension EmergencyManager: SirenPlayerDelegate {\n    // called when siren starts\n    func sirenStarted() {\n        // do nothing\n    }\n    \n    // called when siren stops\n    func sirenStopped() {\n        // do nothing\n    }\n    \n    // called when siren availability changes\n    func sirenAvailabilityUpdate(_ isAvailable: Bool) {\n        delegate?.sosAvailabilityUpdate(isSOSAvailable)\n    }\n}\n\nextension EmergencyManager: SOSBeaconDelegate {\n    // called when sos beacon starts\n    func beaconStarted() {\n        // do nothing\n    }\n    \n    // called when sos beacon stops\n    func beaconStopped() {\n        // do nothing\n    }\n    \n    // called when sos beacon availability changes\n    func beaconAvailabilityUpdate(_ isAvailable: Bool) {\n        delegate?.sosAvailabilityUpdate(isSOSAvailable)\n    }\n}\n\nextension EmergencyManager: DisasterDetectorDelegate {\n    // called when an disaster is detected\n    func disasterDetected() {\n        // handle disaster detected\n        delegate?.disasterDetected()\n        \n        // schedule a notification if app is in background\n        if UIApplication.shared.applicationState != .active {\n            scheduleDisasterNotification()\n        }\n        \n        // start confirmation timer\n        startConfirmationTimer()\n    }\n    \n    // called when disaster detection starts\n    func disasterDetectionStarted() {\n        // do nothing\n    }\n    \n    // called when disaster detection stops\n    func disasterDetectionStopped() {\n        // do nothing\n    }\n    \n    // called when disaster detector availablility changes\n    func disasterDetectorAvailabilityUpdate(\n        _ isAvailable: Bool\n    ) {\n        NSLog(\"EmergencyManager: disaster detector\\(isAvailable ? \"\" : \" not\") available\")\n        \n        delegate?.detectorAvailabilityUpdate(isDetectorAvailable)\n        \n        if !isAvailable {\n            stopDetector()\n        } else {\n            startDetector()\n        }\n    }\n}\n\nextension EmergencyManager: UNUserNotificationCenterDelegate {\n    // requests notification permissions\n    private func requestNotificationPermissions() {\n        UNUserNotificationCenter\n            .current()\n            .requestAuthorization(\n                options: [.alert, .sound, .badge]\n            ) { granted, error in\n                if let error = error {\n                    print(\"EmergencyManager: notification permission error: \\(error)\")\n                }\n            }\n    }\n    \n    // sets up notification categories and actions\n    private func setupNotificationCategories() {\n        let respondAction = UNNotificationAction(\n            identifier: \"RESPOND_ACTION\",\n            title: \"I'm Okay\",\n            options: [.foreground]\n        )\n        \n        let helpAction = UNNotificationAction(\n            identifier: \"NEED_HELP_ACTION\",\n            title: \"Need Help\",\n            options: [.destructive]\n        )\n        \n        let category = UNNotificationCategory(\n            identifier: \"DISASTER_CATEGORY\",\n            actions: [respondAction, helpAction],\n            intentIdentifiers: [],\n            options: []\n        )\n        \n        UNUserNotificationCenter.current().setNotificationCategories([category])\n    }\n    \n    // schedules a notification when a disaster is detected\n    private func scheduleDisasterNotification() {\n        let content = UNMutableNotificationContent()\n        \n        content.title = \"Disaster Detected\"\n        content.body = \"Are you okay?\"\n        content.sound = .default\n        content.categoryIdentifier = \"DISASTER_CATEGORY\"\n        \n        let trigger = UNTimeIntervalNotificationTrigger(\n            timeInterval: 1,\n            repeats: false\n        )\n        \n        let request = UNNotificationRequest(\n            identifier: Constants.DisasterResponseNotificationID,\n            content: content,\n            trigger: trigger\n        )\n        \n        UNUserNotificationCenter.current().add(request) { error in\n            if let error = error {\n                print(\"EmergencyManager: failed to schedule notification: \\(error)\")\n            }\n        }\n    }\n    \n    // handle notification when app is in foreground\n    func userNotificationCenter(\n        _ center: UNUserNotificationCenter,\n        willPresent notification: UNNotification,\n        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void\n    ) {\n        completionHandler([.banner, .sound])\n    }\n    \n    // handle user response to notification\n    func userNotificationCenter(\n        _ center: UNUserNotificationCenter,\n        didReceive response: UNNotificationResponse,\n        withCompletionHandler completionHandler: @escaping () -> Void\n    ) {\n        switch response.actionIdentifier {\n        case \"RESPOND_ACTION\", UNNotificationDefaultActionIdentifier:\n            stopSOS()\n            \n        case \"NEED_HELP_ACTION\":\n            startSOS()\n            \n        default:\n            break\n        }\n        \n        completionHandler()\n    }\n}\n\nprotocol EmergencyManagerDelegate: AnyObject {\n    func disasterDetected()\n    \n    func detectorStarted()\n    func detectorStopped()\n    \n    func detectorAvailabilityUpdate(_ isAvailable: Bool)\n    \n    func sosStarted()\n    func sosStopped()\n    \n    func sosAvailabilityUpdate(_ isAvailable: Bool)\n}\n"
  },
  {
    "path": "ios/Igatha/Services/LocationManager.swift",
    "content": "//\n//  LocationManager.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 17/10/2024.\n//\n\nimport Foundation\nimport CoreLocation\n\nclass LocationManager: NSObject {\n    weak var delegate: LocationManagerDelegate?\n    \n    private var locationManager: CLLocationManager!\n    \n    public var isAvailable: Bool = false\n    public var isActive: Bool = false\n    \n    override init() {\n        super.init()\n        \n        locationManager = CLLocationManager()\n        locationManager.delegate = self\n        \n        // background updates to read core motion sensors\n        locationManager.allowsBackgroundLocationUpdates = true\n        \n        // ensure the location updates arent paused by system\n        locationManager.pausesLocationUpdatesAutomatically = false\n        \n        // uses radio signals; low battery consumption\n        locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers\n        \n        locationManager.requestAlwaysAuthorization()\n    }\n    \n    deinit {\n        stopUpdates()\n    }\n    \n    func startUpdates() {\n        guard\n            isAvailable\n                && !isActive\n        else { return }\n        \n        locationManager.startUpdatingLocation()\n        \n        NSLog(\"LocationManager: started updates\")\n        \n        delegate?.locationUpdatesStarted()\n    }\n    \n    func stopUpdates() {\n        locationManager.stopUpdatingLocation()\n        \n        NSLog(\"LocationManager: stopped updates\")\n        \n        delegate?.locationUpdatesStopped()\n    }\n}\n\nextension LocationManager: CLLocationManagerDelegate {\n    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {\n        switch manager.authorizationStatus {\n        case .notDetermined:\n            locationManager.requestAlwaysAuthorization()\n            return\n            \n        case .authorizedAlways:\n            isAvailable = true\n            \n        case .authorizedWhenInUse, .restricted, .denied:\n            isAvailable = false\n            \n        @unknown default:\n            isAvailable = false\n        }\n        \n        NSLog(\"LocationManager:\\(isAvailable ? \"\" : \" not\") available\")\n        \n        delegate?.locationManagerAvailabilityUpdate(isAvailable)\n        \n        if !isAvailable {\n            stopUpdates()\n        }\n    }\n    \n    func locationManager(\n        _ manager: CLLocationManager,\n        didFailWithError error: Error\n    ) {\n        isAvailable = false\n    }\n    \n    func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) {\n        isActive = true\n    }\n    \n    func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {\n        isActive = false\n    }\n}\n\nprotocol LocationManagerDelegate: AnyObject {\n    func locationUpdatesStarted()\n    func locationUpdatesStopped()\n    \n    func locationManagerAvailabilityUpdate(_ isAvailable: Bool)\n}\n"
  },
  {
    "path": "ios/Igatha/Services/NotificationManager.swift",
    "content": "//\n//  NotificationManager.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 22/04/2025.\n//\n\nimport Foundation\nimport UserNotifications\nimport SwiftUI\n\nclass NotificationManager: NSObject, UNUserNotificationCenterDelegate {\n    static let shared = NotificationManager()\n\n    // setup is called when the app is launched\n    func setup() async {\n        do {\n            try await requestAuthorization()\n            await scheduleFeedbackRequestNotification()\n        } catch {\n            NSLog(\"Error setting up notification manager: \\(error)\")\n        }\n    }\n\n    private override init() {\n        super.init()\n\n        // Set the delegate for the notification center\n        UNUserNotificationCenter.current().delegate = self\n    }\n\n    // requestAuthorization requests notification permission\n    private func requestAuthorization() async throws {\n        let options: UNAuthorizationOptions = [.alert, .sound, .badge]\n        try await UNUserNotificationCenter.current().requestAuthorization(options: options)\n    }\n\n    // scheduleFeedbackRequestNotification schedules the feedback request notification\n    private func scheduleFeedbackRequestNotification() async {\n        // Ignore if we have already scheduled the feedback request notification\n        if UserDefaults.standard.object(\n            forKey: Constants.Notifications.Feedback.TimestampKey\n        ) != nil {\n            return\n        }\n\n        let timestamp = Date()\n\n        NSLog(\"Scheduling feedback request notification at \\(timestamp)\")\n\n        // Store the timestamp when the notification was scheduled\n        UserDefaults.standard.set(\n            timestamp,\n            forKey: Constants.Notifications.Feedback.TimestampKey\n        )\n\n        // Create the notification content\n        let content = UNMutableNotificationContent()\n        content.title = \"Tell us why you use Igatha\"\n        content.body = \"Help us improve it for you and others\"\n        content.sound = .default\n        content.userInfo = [\n            Constants.DeepLink.Key: Constants.Notifications.Feedback.Link.Value\n        ]\n\n        // Schedule the notification for the future\n        let trigger = UNTimeIntervalNotificationTrigger(\n            timeInterval: Constants.Notifications.Feedback.TriggerDelay,\n            repeats: false\n        )\n\n        // Create the notification request\n        let request = UNNotificationRequest(\n            identifier: Constants.Notifications.Feedback.Id,\n            content: content,\n            trigger: trigger\n        )\n\n        // Add the request to the notification center with async/await\n        do {\n            try await UNUserNotificationCenter.current().add(request)\n        } catch {\n            NSLog(\"Error scheduling feedback request notification: \\(error)\")\n        }\n    }\n\n    // userNotificationCenter handles the notification response\n    func userNotificationCenter(\n        _ center: UNUserNotificationCenter,\n        didReceive response: UNNotificationResponse,\n        withCompletionHandler completionHandler: @escaping () -> Void\n    ) {\n        // Get the identifier of the notification\n        let id = response.notification.request.identifier\n\n        // Handle the feedback request notification\n        if id == Constants.Notifications.Feedback.Id {\n            if\n                let deepLink = response.notification.request.content.userInfo[Constants.DeepLink.Key] as? String,\n                deepLink == Constants.Notifications.Feedback.Link.Value\n            {\n                // Post notification on the main thread\n                Task {\n                    await MainActor.run {\n                        NotificationCenter.default.post(\n                            name: NSNotification.Name(Constants.Notifications.Feedback.Link.Name),\n                            object: nil\n                        )\n                    }\n                }\n            }\n        }\n\n        completionHandler()\n    }\n\n    // userNotificationCenter handles the notification presentation\n    func userNotificationCenter(\n        _ center: UNUserNotificationCenter,\n        willPresent notification: UNNotification,\n        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void\n    ) {\n        // Show the notification even if the app is in foreground\n        completionHandler([.banner, .sound])\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Services/ProximityScanner.swift",
    "content": "//\n//  ProximityScanner.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 06/10/2024.\n//\n\nimport Foundation\nimport CoreBluetooth\n\nclass ProximityScanner: NSObject {\n    weak var delegate: ProximityScannerDelegate?\n    \n    private var centralManager: CBCentralManager!\n    \n    public var isActive: Bool {\n        return centralManager.isScanning\n    }\n    public var isAvailable: Bool = false\n    \n    override init() {\n        super.init()\n        \n        centralManager = CBCentralManager(delegate: self, queue: nil)\n    }\n    \n    deinit {\n        stopScanning()\n    }\n}\n\nextension ProximityScanner: CBCentralManagerDelegate {\n    // called when the central state changes\n    func centralManagerDidUpdateState(_ central: CBCentralManager) {\n        switch central.state {\n        case .poweredOn:\n            isAvailable = true\n            \n        case .poweredOff, .resetting, .unauthorized, .unsupported, .unknown:\n            isAvailable = false\n            \n        @unknown default:\n            isAvailable = false\n        }\n        \n        delegate?.scannerAvailabilityUpdate(isAvailable)\n        \n        if !isAvailable {\n            stopScanning()\n        } else {\n            startScanning()\n        }\n    }\n    \n    func startScanning() {\n        guard\n            isAvailable\n                && !isActive\n        else {\n            return\n        }\n        \n        // start scanning for peripherals\n        centralManager.scanForPeripherals(\n            withServices: [\n                Constants.SOSBeaconServiceID\n            ],\n            options: [\n                // allowing duplicates updates the rssi value\n                CBCentralManagerScanOptionAllowDuplicatesKey: true\n            ]\n        )\n    }\n    \n    func stopScanning() {\n        centralManager.stopScan()\n    }\n    \n    // called when a peripheral is detected\n    func centralManager(\n        _ central: CBCentralManager,\n        didDiscover peripheral: CBPeripheral,\n        advertisementData: [String: Any],\n        rssi RSSI: NSNumber\n    ) {\n        delegate?.scannedDevice(\n            Device(\n                id: peripheral.identifier,\n                rssi: RSSI.doubleValue,\n                lastSeen: Date()\n            )\n        )\n    }\n}\n\nprotocol ProximityScannerDelegate: AnyObject {\n    func scannedDevice(_ device: Device)\n    \n    func scannerAvailabilityUpdate(_ isAvailable: Bool)\n}\n"
  },
  {
    "path": "ios/Igatha/Services/SOSBeacon.swift",
    "content": "//\n//  SOSBeacon.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 11/10/2024.\n//\n\nimport Foundation\nimport CoreBluetooth\n\nclass SOSBeacon: NSObject {\n    weak var delegate: SOSBeaconDelegate?\n    \n    private var peripheralManager: CBPeripheralManager!\n    \n    public var isActive: Bool {\n        return peripheralManager.isAdvertising\n    }\n    public var isAvailable: Bool = false\n    \n    override init() {\n        super.init()\n        \n        peripheralManager = CBPeripheralManager(\n            delegate: self,\n            queue: nil,\n            options: [\n                CBPeripheralManagerOptionRestoreIdentifierKey: Constants.SOSBeaconRestoreID\n            ]\n        )\n    }\n    \n    deinit {\n        stopBroadcasting()\n    }\n    \n    // start broadcasting (or re-broadcasting) as a peipheral\n    public func startBroadcasting() {\n        guard\n            isAvailable\n                && !isActive\n        else { return }\n        \n        // start broadcasting\n        peripheralManager.startAdvertising(\n            [\n                CBAdvertisementDataServiceUUIDsKey: [\n                    Constants.SOSBeaconServiceID\n                ]\n            ]\n        )\n        \n        delegate?.beaconStarted()\n    }\n    \n    // stop broadcasting as a peripheral\n    public func stopBroadcasting() {\n        peripheralManager.stopAdvertising()\n        peripheralManager.removeAllServices()\n        \n        delegate?.beaconStopped()\n    }\n}\n\nextension SOSBeacon: CBPeripheralManagerDelegate {\n    // called when the peripheral state changes\n    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {\n        switch peripheral.state {\n        case .poweredOn:\n            isAvailable = true\n            \n        case .poweredOff, .resetting, .unauthorized, .unsupported, .unknown:\n            isAvailable = false\n            \n        @unknown default:\n            isAvailable = false\n        }\n        \n        delegate?.beaconAvailabilityUpdate(isAvailable)\n        \n        if !isAvailable {\n            stopBroadcasting()\n        }\n    }\n    \n    func peripheralManager(\n        _ peripheral: CBPeripheralManager,\n        willRestoreState dict: [String: Any]\n    ) {\n        // check if it was advertising\n        guard\n            let serviceUUIDs = dict[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID],\n            serviceUUIDs.contains(Constants.SOSBeaconServiceID)\n        else {\n            return\n        }\n        \n        startBroadcasting()\n    }\n}\n\nprotocol SOSBeaconDelegate: AnyObject {\n    func beaconStarted()\n    func beaconStopped()\n    \n    func beaconAvailabilityUpdate(_ isAvailable: Bool)\n}\n"
  },
  {
    "path": "ios/Igatha/Services/SirenPlayer.swift",
    "content": "//\n//  SirenPlayer.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 12/10/2024.\n//\n\nimport AVFoundation\n\nclass SirenPlayer {\n    weak var delegate: SirenPlayerDelegate?\n    \n    private var audioEngine: AVAudioEngine?\n    private var playerNode: AVAudioPlayerNode?\n    \n    public var isActive: Bool {\n        return playerNode?.isPlaying ?? false\n    }\n    public var isAvailable: Bool = false\n    \n    init() {\n        checkSupport()\n    }\n    \n    deinit {\n        stopSiren()\n    }\n    \n    private func checkSupport() {\n        let audioSession = AVAudioSession.sharedInstance()\n        \n        var isAvailable = true\n        do {\n            try audioSession.setCategory(\n                // needed to default to speaker\n                .playAndRecord,\n                mode: .default,\n                options: [\n                    // plays audio on phone speaker\n                    // not connected bluetooth devices\n                    .defaultToSpeaker,\n                    // plays alongside others\n                    .mixWithOthers,\n                    // lowers volume of others\n                    .duckOthers\n                ]\n            )\n            \n            // override the output audio port to speaker\n            try audioSession.overrideOutputAudioPort(.speaker)\n            \n            // toggle audio session for testing\n            try audioSession.setActive(true)\n            try audioSession.setActive(false)\n        } catch {\n            print(\"SirenPlayer: error with audio session: \\(error)\")\n            \n            isAvailable = false\n        }\n        \n        self.isAvailable = isAvailable\n        delegate?.sirenAvailabilityUpdate(isAvailable)\n    }\n    \n    func startSiren() {\n        guard\n            isAvailable\n                && !isActive\n        else { return }\n        \n        audioEngine = AVAudioEngine()\n        playerNode = AVAudioPlayerNode()\n        \n        guard\n            let audioEngine = audioEngine,\n            let playerNode = playerNode\n        else { return }\n        \n        audioEngine.attach(playerNode)\n        \n        let audioFormat = AVAudioFormat(\n            standardFormatWithSampleRate: 44100,\n            channels: 1\n        )!\n        \n        let mainMixer = audioEngine.mainMixerNode\n        \n        audioEngine.connect(\n            playerNode,\n            to: mainMixer,\n            format: audioFormat\n        )\n        \n        let buffer = createSirenBuffer(format: audioFormat)\n        \n        playerNode.scheduleBuffer(\n            buffer,\n            at: nil,\n            options: .loops,\n            completionHandler: nil\n        )\n        \n        let audioSession = AVAudioSession.sharedInstance()\n        do {\n            // activate audio session\n            try audioSession.setActive(true)\n            \n            // start audio engine\n            try audioEngine.start()\n            \n            // play the siren buffer\n            playerNode.play()\n        } catch {\n            print(\"SirenPlayer: error starting siren: \\(error)\")\n            \n            stopSiren()\n            return\n        }\n        \n        delegate?.sirenStarted()\n    }\n    \n    func stopSiren() {\n        // stop the siren buffer\n        playerNode?.stop()\n        playerNode = nil\n        \n        // stop audio engine\n        audioEngine?.stop()\n        audioEngine = nil\n        \n        let audioSession = AVAudioSession.sharedInstance()\n        do {\n            // deactivate audio session\n            try audioSession.setActive(false)\n        } catch {\n            print(\"SirenPlayer: error stopping siren: \\(error)\")\n            \n            return\n        }\n        \n        delegate?.sirenStopped()\n    }\n    \n    // creates the siren sound\n    private func createSirenBuffer(format: AVAudioFormat) -> AVAudioPCMBuffer {\n        // duration of one siren cycle in seconds\n        let duration: Double = 2.0\n        \n        let sampleRate = format.sampleRate\n        let totalSamples = Int(sampleRate * duration)\n        \n        let buffer = AVAudioPCMBuffer(\n            pcmFormat: format,\n            frameCapacity: AVAudioFrameCount(totalSamples)\n        )!\n        \n        buffer.frameLength = AVAudioFrameCount(totalSamples)\n        \n        let channels = buffer.floatChannelData!\n        let channel = channels[0]\n        \n        let frequencyStart: Float = 600.0 // starting frequency in Hz\n        let frequencyEnd: Float = 1200.0  // ending frequency in Hz\n        let amplitude: Float = 1.0        // volume\n        \n        for sampleIndex in 0..<totalSamples {\n            let t = Float(sampleIndex) / Float(sampleRate)\n            \n            // modulate frequency to create siren effect\n            // goes from 0 to 1 to 0 over duration\n            let modulation = sin(Float.pi * t / Float(duration))\n            \n            let frequency = frequencyStart + modulation * (frequencyEnd - frequencyStart)\n            let sample = sin(2.0 * Float.pi * frequency * t) * amplitude\n            \n            channel[sampleIndex] = sample\n        }\n        \n        return buffer\n    }\n}\n\nprotocol SirenPlayerDelegate: AnyObject {\n    func sirenStarted()\n    func sirenStopped()\n    \n    func sirenAvailabilityUpdate(_ isAvailable: Bool)\n}\n"
  },
  {
    "path": "ios/Igatha/ViewModels/ContentViewModel.swift",
    "content": "//\n//  ViewModel.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 14/10/2024.\n//\n\nimport SwiftUI\n\nclass ContentViewModel: ObservableObject {\n    @Published public var isSOSAvailable: Bool = false\n    @Published public var isSOSActive: Bool = false\n    \n    @Published public var activeAlert: AlertType? = nil\n    \n    @Published private var devicesMap: [String: Device] = [:]\n    public var devices: [Device] {\n        return devicesMap.values.sorted {\n            $0.rssi > $1.rssi\n        }\n    }\n    \n    private let emergencyManager: EmergencyManager\n    private let proximityScanner: ProximityScanner\n    \n    init() {\n        emergencyManager = EmergencyManager.shared\n        proximityScanner = ProximityScanner()\n        \n        emergencyManager.delegate = self\n        proximityScanner.delegate = self\n        \n        updateSOSAvailability()\n    }\n    \n    deinit {\n        proximityScanner.stopScanning()\n    }\n    \n    func startDetector() {\n        DispatchQueue.global(qos: .background).async {\n            self.emergencyManager.startDetector()\n        }\n    }\n    \n    func stopDetector() {\n        DispatchQueue.global(qos: .background).async {\n            self.emergencyManager.stopDetector()\n        }\n    }\n    \n    func startSOS() {\n        DispatchQueue.global(qos: .background).async {\n            self.emergencyManager.startSOS()\n        }\n    }\n    \n    func stopSOS() {\n        DispatchQueue.global(qos: .background).async {\n            self.emergencyManager.stopSOS()\n        }\n    }\n    \n    func updateSOSAvailability(\n        isAvailable: Bool? = nil,\n        isActive: Bool? = nil\n    ) {\n        DispatchQueue.main.async {\n            self.isSOSAvailable = (\n                isAvailable\n                ?? self.emergencyManager.isSOSAvailable\n            )\n            \n            self.isSOSActive = (\n                isActive\n                ?? self.emergencyManager.isSOSActive\n            )\n        }\n    }\n}\n\nextension ContentViewModel: EmergencyManagerDelegate {\n    func disasterDetected() {\n        DispatchQueue.main.async {\n            self.activeAlert = .disasterDetected\n        }\n    }\n    \n    func detectorStarted() {\n        // do nothing\n    }\n    \n    func detectorStopped() {\n        // do nothing\n    }\n    \n    func detectorAvailabilityUpdate(_ isAvailable: Bool) {\n        // do nothing\n    }\n    \n    func sosStarted() {\n        updateSOSAvailability(isActive: true)\n    }\n    \n    func sosStopped() {\n        updateSOSAvailability(isActive: false)\n    }\n    \n    func sosAvailabilityUpdate(_ isAvailable: Bool) {\n        updateSOSAvailability(isAvailable: isAvailable)\n    }\n}\n\nextension ContentViewModel: ProximityScannerDelegate {\n    func scannedDevice(_ device: Device) {\n        DispatchQueue.main.async {\n            if !self.devicesMap.keys.contains(device.id) {\n                self.devicesMap[device.id] = device\n            }\n            \n            self.devicesMap[device.id]?.update(rssi: device.rssi)\n        }\n    }\n    \n    func scannerAvailabilityUpdate(_ isAvailable: Bool) {\n        guard isAvailable else {\n            proximityScanner.stopScanning()\n            return\n        }\n        \n        proximityScanner.startScanning()\n    }\n}\n\nenum AlertType: Identifiable {\n    case sosConfirmation\n    case disasterDetected\n    \n    var id: Int {\n        switch self {\n        case .sosConfirmation:\n            return 1\n        case .disasterDetected:\n            return 2\n        }\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/ViewModels/FeedbackFormViewModel.swift",
    "content": "//\n//  FeedbackFormViewModel.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 20/04/2025.\n//\n\nimport SwiftUI\nimport Foundation\n\n// FeedbackFormViewModel handles all logic for the feedback form view.\n@MainActor\nclass FeedbackFormViewModel: ObservableObject {\n    \n    // formState stores the state of the form.\n    @Published public var formState: FormState = .idle\n    \n    // submissionResult stores the result of the form submission.\n    @Published public var submissionResult: SubmissionResult?\n    \n    // usageReasons stores all the usage reasons the user selected.\n    @Published public var usageReasons: Set<UsageReason> = []\n    \n    // Form bindings.\n    @Published public var customUsage: String = \"\"\n    @Published public var ideas: String = \"\"\n    @Published public var email: String = \"\"\n    \n    // hasCustomUsage checks if the user selected the \"other\" usage reason.\n    public var hasCustomUsage: Bool {\n        return usageReasons.contains(.other)\n    }\n    \n    // isUsageReasonSelected checks if the usage reason is selected.\n    public func isUsageReasonSelected(_ reason: UsageReason) -> Bool {\n        return usageReasons.contains(reason)\n    }\n    \n    // toggleUsageReason updates the selected usage reasons.\n    public func toggleUsageReason(_ reason: UsageReason) {\n        // If reason is already selected, remove it.\n        guard !usageReasons.contains(reason) else {\n            usageReasons.remove(reason)\n            return\n        }\n        \n        // Otherwise, add it.\n        usageReasons.insert(reason)\n    }\n    \n    // dismissAlert dismisses submission result alert.\n    public func dismissAlert() {\n        submissionResult = nil\n    }\n    \n    // validateForm validates the form and returns the error if it's invalid.\n    public func validateForm() -> String? {\n        // Form is valid if at least one usage reason is selected.\n        guard !usageReasons.isEmpty else {\n            return \"Please select at least one usage reason.\"\n        }\n        \n        // If \"other\" is selected, custom usage should not be empty.\n        if usageReasons.contains(.other) && customUsage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {\n            return \"Please specify why you chose 'other'.\"\n        }\n        \n        return nil\n    }\n    \n    // submit submits the feedback form.\n    public func submit() async {\n        // Validate the form and return the error if it's invalid.\n        if let errMsg = validateForm() {\n            submissionResult = .err(errMsg)\n            return\n        }\n        \n        // Update form state to submitting.\n        formState = .submitting\n        \n        let feedback = Feedback(\n            usageReasons: usageReasons,\n            customUsage: customUsage,\n            ideas: ideas,\n            email: email\n        )\n        \n        let form = UsageFeedbackGoogleForm(feedback: feedback)\n        \n        // Submit the form asynchronously.\n        do {\n            // Try to submit the form.\n            try await form.submit()\n            \n            // Show the success alert. Updates are safe due to @MainActor.\n            submissionResult = .success\n        } catch {\n            // Generate a truncated reference ID\n            let refId = UUID().uuidString.prefix(8)\n            \n            // Get a descriptive error message\n            let errorMessage = error.localizedDescription.isEmpty ?\n            \"Connection error. Check your internet connection.\" : error.localizedDescription\n            \n            // Log the error with full details for troubleshooting\n            // TODO: Replace local logging with an error reporting service\n            NSLog(\"Error submitting form - Ref: \\(refId) - Details: \\(errorMessage)\")\n            \n            // Show the error alert with reference ID\n            submissionResult = .err(\"Your feedback could not be submitted. Please try again later. (Ref: \\(refId))\")\n        }\n        \n        // Update form state to idle. Updates are safe due to @MainActor.\n        formState = .idle\n    }\n}\n\n// UsageFeedbackGoogleForm has the submission logic for https://forms.gle/rcu3MZjPYww7Fbnh7. [1]\n// This class performs network operations and does not need to be @MainActor.\nclass UsageFeedbackGoogleForm {\n    // formUrl is the URL for the POST request.\n    private let formUrl = URL(string: \"https://docs.google.com/forms/u/0/d/e/1FAIpQLSdCdNYIaPcg2-eAs1Mlvwoa6P5Ijqfdb1hmWlaA-poIKpMDtQ/formResponse\")!\n    \n    // feedback is the feedback field value.\n    private var feedback: Feedback\n    // feedbackField is the feedback field identifier.\n    private let feedbackField = \"entry.457989095\"\n    \n    // init initializes the form fields.\n    init(feedback: Feedback) {\n        self.feedback = feedback\n    }\n    \n    // submit submits the feedback form.\n    public func submit() async throws {\n        // Create the request.\n        var request = URLRequest(url: formUrl)\n        request.httpMethod = \"POST\"\n        \n        // Google requires form to be submitted as a urlencoded-form.\n        request.setValue(\"application/x-www-form-urlencoded\", forHTTPHeaderField: \"Content-Type\")\n        \n        // We pass the feedback to a single field for flexibility.\n        var comps  = URLComponents()\n        comps.queryItems = [ URLQueryItem(name: feedbackField, value: feedback.toJSON()) ]\n        \n        // Add the request body.\n        request.httpBody = comps.percentEncodedQuery?.data(using: .utf8)\n        \n        // Send the request.\n        let (_, response) = try await URLSession.shared.data(for: request)\n        \n        guard (response as? HTTPURLResponse)?.statusCode == 200 else {\n            throw URLError(.badServerResponse)\n        }\n    }\n}\n\n// Feedback stores the form data and converts it to a JSON string.\nstruct Feedback {\n    // Constant form data.\n    private let device = \"ios\"\n    private let key = \"ios-usage-feedback-v1.0.0\"\n    \n    // Dynamic form data.\n    private var usageReasons: Set<UsageReason>\n    private var customUsage: String\n    private var ideas: String\n    private var email: String\n    \n    // init initializes the form data from the view fields.\n    init(\n        usageReasons: Set<UsageReason>,\n        customUsage: String,\n        ideas: String,\n        email: String\n    ) {\n        self.usageReasons = usageReasons\n        self.customUsage = customUsage\n        self.ideas = ideas\n        self.email = email\n    }\n    \n    // toJson converts the form data to a JSON string.\n    public func toJSON() -> String {\n        let dict: [String: Any] = [\n            \"usage\": [\n                \"reasons\": usageReasons.map { $0.displayString },\n                \"custom\": customUsage,\n            ],\n            \"ideas\": ideas,\n            \"email\": email,\n            \"device\": device,\n            \"key\": key,\n        ]\n        \n        guard\n            let data = try? JSONSerialization.data(withJSONObject: dict),\n            let str  = String(data: data, encoding: .utf8)\n        else {\n            // Fallback to empty JSON string, instead of crashing.\n            return \"{}\"\n        }\n        \n        return str\n    }\n}\n\n// FormState stores the state of the form.\nenum FormState {\n    case idle\n    case submitting\n}\n\n// SubmissionResult stores the result of the form submission.\nenum SubmissionResult: Identifiable {\n    // id makes each submission result unique for SwiftUI\n    var id: UUID { UUID() }\n    \n    case success\n    case err(String)\n}\n\n// UsageReason stores all usage reasons and their examples.\nenum UsageReason: Hashable, Identifiable, CaseIterable {\n    case disasterPreparedness\n    case adventureTravel\n    case caregiving\n    case workplaceSafety\n    case regionalConflict\n    case other\n    \n    var id: Self { self }\n    \n    // all cases for iteration\n    static var allCases: [UsageReason] = [\n        .disasterPreparedness, .adventureTravel, .caregiving, .workplaceSafety, .regionalConflict, .other\n    ]\n    \n    // displayString is the primary text for the usage reason.\n    var displayString: String {\n        switch self {\n        case .disasterPreparedness: return \"Disaster Preparedness\"\n        case .adventureTravel: return \"Adventure & Travel\"\n        case .caregiving: return \"Caregiving\"\n        case .workplaceSafety: return \"Workplace Safety\"\n        case .regionalConflict: return \"Regional Conflict or Instability\"\n        case .other: return \"Other\"\n        }\n    }\n    \n    // exampleString is the subtext for the usage reason.\n    var exampleString: String? {\n        switch self {\n        case .disasterPreparedness: return \"e.g., earthquakes, floods, conflicts\"\n        case .adventureTravel: return \"e.g., hiking, biking, remote trips\"\n        case .caregiving: return \"e.g., monitoring elderly or dependents\"\n        case .workplaceSafety: return \"e.g., construction, mining, field jobs\"\n        case .regionalConflict: return \"e.g., war, civil unrest, political instability\"\n        case .other: return \"Please specify below\"\n        }\n    }\n}\n\n/* Notes\n \n [1]:\n I am aware that the Google form can be spammed.\n I care most about emails, and this helps me reach users directly.\n I made sure there's no long term or financial damage from spam.\n This cheap implementation to get emails here outweighs any other.\n \n */\n"
  },
  {
    "path": "ios/Igatha/ViewModels/SettingsViewModel.swift",
    "content": "//\n//  SettingsViewModel.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 17/10/2024.\n//\n\nimport SwiftUI\n\nclass SettingsViewModel: ObservableObject {\n    @AppStorage(Constants.DisasterDetectionSettingsKey)\n    var disasterDetectionEnabled: Bool = true {\n        didSet {\n            if disasterDetectionEnabled {\n                DispatchQueue.global(qos: .background).async {\n                    EmergencyManager.shared.startDetector()\n                }\n            } else {\n                DispatchQueue.global(qos: .background).async {\n                    EmergencyManager.shared.stopDetector()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Views/ContentView.swift",
    "content": "//\n//  ContentView.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 05/10/2024.\n//\n\nimport SwiftUI\n\nstruct ContentView: View {\n    @StateObject private var viewModel = ContentViewModel()\n    @StateObject private var deepLinkHandler = DeepLinkHandler.shared\n\n    var body: some View {\n        NavigationView {\n            VStack {\n                // list of devices\n                DeviceListView(\n                    devices: viewModel.devices\n                )\n                .padding(.bottom, 8)\n\n                Spacer()\n\n                // sos button\n                Button(action: {\n                    if viewModel.isSOSActive {\n                        viewModel.stopSOS()\n                    } else {\n                        // show confirmation alert\n                        viewModel.activeAlert = .sosConfirmation\n                    }\n                }) {\n                    Text(\n                        viewModel.isSOSAvailable\n                        ? viewModel.isSOSActive\n                        ? \"Stop SOS\"\n                        : \"Send SOS\"\n                        : \"SOS Unavailable\"\n                    )\n                    .font(.headline)\n                    .foregroundColor(.white)\n                    .frame(maxWidth: .infinity)\n                    .padding()\n                    .background(\n                        viewModel.isSOSActive\n                        ? Color.gray\n                        : Color.red\n                    )\n                    .opacity(\n                        viewModel.isSOSAvailable\n                        ? 1\n                        : 0.75\n                    )\n                    .cornerRadius(8)\n                }\n                .disabled(!viewModel.isSOSAvailable)\n                .padding([.horizontal, .bottom])\n                .animation(.easeInOut, value: viewModel.isSOSActive)\n            }\n            .navigationBarTitleDisplayMode(.inline)\n            .navigationBarItems(\n                trailing: NavigationLink(\n                    destination: SettingsView(),\n                    isActive: $deepLinkHandler.showSettings\n                ) {\n                    Image(systemName: \"gearshape\")\n                        .imageScale(.large)\n                }\n            )\n        }\n        .alert(item: $viewModel.activeAlert) { alertType in\n            switch alertType {\n            case .sosConfirmation:\n                return Alert(\n                    title: Text(\"Are you sure?\"),\n                    message: Text(\"This will broadcast your location and start a loud siren.\"),\n                    primaryButton: .destructive(Text(\"Yes\")) {\n                        viewModel.startSOS()\n\n                        viewModel.activeAlert = nil\n                    },\n                    secondaryButton: .cancel() {\n                        viewModel.activeAlert = nil\n                    }\n                )\n            case .disasterDetected:\n                return Alert(\n                    title: Text(\"Disaster Detected\"),\n                    message: Text(\"Are you okay?\"),\n                    primaryButton: .default(Text(\"I'm Okay\")) {\n                        viewModel.stopSOS()\n\n                        viewModel.activeAlert = nil\n                    },\n                    secondaryButton: .destructive(Text(\"Need Help\")) {\n                        viewModel.startSOS()\n\n                        viewModel.activeAlert = nil\n                    }\n                )\n            }\n        }\n        .navigationBarTitleDisplayMode(.inline)\n        .navigationViewStyle(StackNavigationViewStyle())\n    }\n}\n\n#Preview {\n    ContentView()\n}\n"
  },
  {
    "path": "ios/Igatha/Views/DeviceDetailView.swift",
    "content": "//\n//  DeviceDetailView.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 13/10/2024.\n//\n\nimport SwiftUI\n\nstruct DeviceDetailView: View {\n    @ObservedObject var device: Device\n    \n    private var timeSinceLastSeen: String {\n        let interval = Date().timeIntervalSince(device.lastSeen)\n        \n        let minute = 60.0\n        let hour = 60.0 * minute\n        let day = 24.0 * hour\n        let week = 7.0 * day\n        \n        if interval < minute {\n            let seconds = Int(interval)\n            return \"\\(seconds) second\\(seconds != 1 ? \"s\" : \"\") ago\"\n        } else if interval < hour {\n            let minutes = Int(interval / minute)\n            return \"\\(minutes) minute\\(minutes != 1 ? \"s\" : \"\") ago\"\n        } else if interval < day {\n            let hours = Int(interval / hour)\n            return \"\\(hours) hour\\(hours != 1 ? \"s\" : \"\") ago\"\n        } else if interval < week {\n            let days = Int(interval / day)\n            return \"\\(days) day\\(days != 1 ? \"s\" : \"\") ago\"\n        } else {\n            let weeks = Int(interval / week)\n            return \"\\(weeks) week\\(weeks != 1 ? \"s\" : \"\") ago\"\n        }\n    }\n    \n    var body: some View {\n        TimelineView(.periodic(from: Date(), by: 60)) { context in\n            let currentDate = context.date\n            let interval = currentDate.timeIntervalSince(device.lastSeen)\n            \n            let minute = 60.0\n            let hour = 60.0 * minute\n            let day = 24.0 * hour\n            let week = 7.0 * day\n            \n            var timeSinceLastSeen: String {\n                if interval < minute {\n                    let seconds = Int(interval)\n                    return \"\\(seconds) second\\(seconds != 1 ? \"s\" : \"\") ago\"\n                } else if interval < hour {\n                    let minutes = Int(interval / minute)\n                    return \"\\(minutes) minute\\(minutes != 1 ? \"s\" : \"\") ago\"\n                } else if interval < day {\n                    let hours = Int(interval / hour)\n                    return \"\\(hours) hour\\(hours != 1 ? \"s\" : \"\") ago\"\n                } else if interval < week {\n                    let days = Int(interval / day)\n                    return \"\\(days) day\\(days != 1 ? \"s\" : \"\") ago\"\n                } else {\n                    let weeks = Int(interval / week)\n                    return \"\\(weeks) week\\(weeks != 1 ? \"s\" : \"\") ago\"\n                }\n            }\n            \n            Form {\n                Section {\n                    HStack {\n                        Text(\"Name\")\n                        \n                        Spacer()\n                        \n                        Text(device.shortName)\n                            .foregroundColor(.secondary)\n                    }\n                    \n                    HStack {\n                        Text(\"ID\")\n                        \n                        Spacer()\n                        \n                        Text(device.id)\n                            .foregroundColor(.secondary)\n                            .multilineTextAlignment(.trailing)\n                            .font(\n                                .system(\n                                    .subheadline,\n                                    design: .monospaced\n                                )\n                            )\n                    }\n                } header: {\n                    Text(\"Identity\")\n                        .padding(.vertical, 4)\n                } footer: {\n                    Text(\"Identity is pseudonymized for privacy.\")\n                        .padding(.vertical, 4)\n                }\n                \n                Section {\n                    HStack {\n                        Text(\"Distance\")\n                        \n                        Spacer()\n                        \n                        Text(\"\\(String(format: \"%.1f\", device.estimateDistance())) meters\")\n                            .font(\n                                .system(\n                                    .subheadline,\n                                    design: .monospaced\n                                )\n                            )\n                            .foregroundColor(.secondary)\n                    }\n                } header: {\n                    Text(\"Location\")\n                        .padding(.vertical, 4)\n                } footer: {\n                    Text(\"Location is limited by used tech. Direction is not available. Distance is approximate and varies due to signal fluctuations. It is for general guidance only.\")\n                        .padding(.vertical, 4)\n                }\n                \n                Section {\n                    HStack {\n                        Text(\"Last Seen\")\n                        \n                        Spacer()\n                        \n                        Text(timeSinceLastSeen)\n                            .foregroundColor(.secondary)\n                    }\n                } header: {\n                    Text(\"Status\")\n                        .padding(.vertical, 4)\n                } footer: {\n                    Text(\"Status shows if the device is active and in range.\")\n                        .padding(.vertical, 4)\n                }\n            }\n            .navigationTitle(\"Device Details\")\n        }\n    }\n}\n\nstruct DeviceDetailView_Previews: PreviewProvider {\n    static var previews: some View {\n        let mockDevice = Device(\n            id: UUID(),\n            rssi: -40\n        )\n        \n        return DeviceDetailView(\n            device: mockDevice\n        )\n        .previewLayout(\n            .sizeThatFits\n        )\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Views/DeviceListView.swift",
    "content": "//\n//  DeviceListView.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 13/10/2024.\n//\n\nimport SwiftUI\n\nstruct DeviceListView: View {\n    let devices: [Device]\n    \n    var body: some View {\n        List {\n            Section {\n                if devices.isEmpty {\n                    Text(\n                        \"No devices found nearby.\"\n                    )\n                    .foregroundColor(.gray)\n                    .padding()\n                } else {\n                    ForEach(devices) { device in\n                        NavigationLink(\n                            destination: DeviceDetailView(device: device)\n                        ) {\n                            DeviceRowView(device: device)\n                        }\n                    }\n                }\n            } header: {\n                Text(\"People Seeking Help\")\n                    .padding(.vertical, 4)\n            } footer: {\n                Text(\"Note: Distance is approximate and varies due to signal fluctuations. It is for general guidance only.\")\n                    .padding(.vertical, 4)\n            }\n        }\n        .listStyle(.automatic)\n    }\n}\n\nstruct DeviceListView_Previews: PreviewProvider {\n    static var previews: some View {\n        // Creating mock devices\n        let mockDevices = [\n            Device(\n                id: UUID(),\n                rssi: -40\n            ),\n            Device(\n                id: UUID(),\n                rssi: -60,\n                lastSeen: Date().addingTimeInterval(-600)\n            ),\n            Device(\n                id: UUID(),\n                rssi: -75\n            ),\n            Device(\n                id: UUID(),\n                rssi: -85\n            )\n        ]\n        \n        return DeviceListView(\n            devices: mockDevices\n        )\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Views/DeviceRowView.swift",
    "content": "//\n//  DeviceRowView.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 13/10/2024.\n//\n\nimport SwiftUI\n\nstruct DeviceRowView: View {\n    @ObservedObject var device: Device\n    \n    var body: some View {\n        TimelineView(.periodic(from: Date(), by: 30)) { context in\n            let currentDate = context.date\n            let isStale = device.lastSeen < currentDate.addingTimeInterval(-300)\n            \n            HStack(spacing: 16) {\n                // device icon\n                Image(systemName: \"person.circle.fill\")\n                    .resizable()\n                    .scaledToFit()\n                    .frame(width: 40, height: 40)\n                    .foregroundColor(.secondary)\n                \n                VStack(alignment: .leading, spacing: 4) {\n                    // device short name\n                    Text(device.shortName)\n                        .font(.headline)\n                        .foregroundColor(.primary)\n                    \n                    // device distance\n                    Text(\n                        \"\\(String(format: \"%.1f\", device.estimateDistance())) meters away\"\n                    )\n                    .font(\n                        .system(\n                            .subheadline,\n                            design: .monospaced\n                        )\n                    )\n                    .foregroundColor(.primary)\n                }\n            }\n            .padding(.vertical, 4)\n            .contentShape(Rectangle())\n            .opacity(isStale ? 0.4 : 1.0)\n            .animation(.easeInOut, value: isStale)\n        }\n    }\n}\n\nstruct DeviceRowView_Previews: PreviewProvider {\n    static var previews: some View {\n        // Creating mock device\n        let mockDevice = Device(\n            id: UUID(),\n            rssi: -40\n        )\n        \n        return DeviceRowView(\n            device: mockDevice\n        )\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Views/FeedbackButtonView.swift",
    "content": "//\n//  FeedbackRowView.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 20/04/2025.\n//\n\nimport SwiftUI\n\nstruct FeedbackButtonView: View {\n    var body: some View {\n        NavigationLink(\n            destination: FeedbackFormView()\n        ) {\n            FeedbackButtonContentView()\n        }\n        .padding(.vertical)\n        .padding(.horizontal, 20)\n        .contentShape(Rectangle())\n        .background(\n            LinearGradient(\n                // pink-ish gradient\n                gradient: Gradient(colors: [\n                    Color.pink.opacity(0.6),\n                    Color.purple.opacity(0.6)\n                ]),\n                // 135 degrees gradient\n                startPoint: .topLeading,\n                endPoint: .bottomTrailing\n            )\n        )\n        .shadow(color: .gray.opacity(0.4), radius: 5, x: 0, y: 2)\n        // Set chevron color to white.\n        .foregroundStyle(.white, .white)\n    }\n}\n\nstruct FeedbackButtonContentView: View {\n    var body: some View {\n        HStack(spacing: 16) {\n            // heart icon\n            Image(systemName: \"heart\")\n                .resizable()\n                .scaledToFit()\n                .frame(width: 30, height: 30)\n                .foregroundColor(.white)\n            \n            VStack(alignment: .leading, spacing: 4) {\n                // text\n                Text(\"Tell us why you use Igatha\")\n                    .font(.headline)\n                    .foregroundColor(.white)\n                \n                // subtext\n                Text(\"It helps us make it more reliable.\")\n                    .font(.subheadline)\n                    .foregroundColor(.white.opacity(0.9))\n            }\n        }\n    }\n}\n\nstruct FeedbackButtonView_Previews: PreviewProvider {\n    static var previews: some View {\n        NavigationView {\n            List {\n                FeedbackButtonView()\n                // removes the section padding around the feedback row\n                    .listRowInsets(EdgeInsets())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Views/FeedbackFormView.swift",
    "content": "//\n//  FeedbackFormView.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 20/04/2025.\n//\n\nimport SwiftUI\n\n// Feedback form to understand why people use Igatha.\nstruct FeedbackFormView: View {\n    // vm is the view model with the form logic.\n    @StateObject private var vm = FeedbackFormViewModel()\n    \n    // dismiss is the environment variable that dismisses the current view.\n    // This is not the best approach, but it's good enough for this case.\n    @Environment(\\.dismiss) private var dismiss\n    \n    // body is the main view of the form.\n    var body: some View {\n        Form {\n            // Usage reasons.\n            Section {\n                ForEach(UsageReason.allCases) { reason in\n                    HStack {\n                        // Reason.\n                        VStack(alignment: .leading) {\n                            // Text.\n                            Text(reason.displayString)\n                            \n                            // Subtext.\n                            if let examples = reason.exampleString {\n                                Text(examples).font(.caption).foregroundColor(.secondary)\n                            }\n                        }\n                        \n                        Spacer()\n                        \n                        // If selected, show checkmark.\n                        if vm.isUsageReasonSelected(reason) {\n                            Image(systemName: \"checkmark\").foregroundColor(.accentColor)\n                        }\n                    }\n                    // Make the row is tappable.\n                    .contentShape(Rectangle())\n                    .onTapGesture {\n                        vm.toggleUsageReason(reason)\n                    }\n                }\n                \n                // If other reason, show text field.\n                if vm.hasCustomUsage {\n                    TextField(\"Please describe\", text: $vm.customUsage)\n                }\n            } header: {\n                Text(\"Why do you use Igatha?\")\n            } footer: {\n                Text(\"Select all that apply.\")\n            }\n            \n            // Ideas.\n            Section {\n                TextEditor(text: $vm.ideas).frame(minHeight: 90)\n            } header: {\n                Text(\"What would make Igatha more helpful?\")\n            } footer: {\n                Text(\"Optional. If you have any ideas, don't hesitate.\")\n            }\n            \n            // Email.\n            Section {\n                TextField(\"Email address\", text: $vm.email)\n                    .keyboardType(.emailAddress)\n                    .autocapitalization(.none)\n            } header: {\n                Text(\"Your email\")\n            } footer: {\n                Text(\"Optional. We'll only contact you for clarifications.\")\n            }\n            \n            // Submit.\n            Section {\n                Button(action: {\n                    guard vm.formState == .idle else { return }\n                    \n                    // Run the async submit function in a Task\n                    Task { await vm.submit() }\n                }) {\n                    HStack {\n                        Text(\"Submit\")\n                        \n                        Spacer()\n                        \n                        // If submitting, show progress view.\n                        if vm.formState == .submitting {\n                            ProgressView()\n                        }\n                    }\n                }\n                .disabled(vm.formState == .submitting)\n            }\n        }\n        .navigationBarTitle(\"Share Feedback\", displayMode: .inline)\n        .alert(item: $vm.submissionResult) { result in\n            switch result {\n            case .success:\n                return Alert(\n                    title: Text(\"Thank you!\"),\n                    message: Text(\"Your feedback helps us improve Igatha.\"),\n                    dismissButton: .cancel(Text(\"Done\")) {\n                        vm.dismissAlert()\n                        dismiss()\n                    }\n                )\n            case .err(let message):\n                return Alert(\n                    title: Text(\"Error\"),\n                    message: Text(message),\n                    dismissButton: .cancel(Text(\"OK\")) {\n                        vm.dismissAlert()\n                    }\n                )\n            }\n        }\n    }\n}\n\nstruct FeedbackFormView_Previews: PreviewProvider {\n    static var previews: some View {\n        NavigationView { FeedbackFormView() }\n    }\n}\n"
  },
  {
    "path": "ios/Igatha/Views/SettingsView.swift",
    "content": "//\n//  SettingsView.swift\n//  Igatha\n//\n//  Created by Nizar Mahmoud on 14/10/2024.\n//\n\nimport SwiftUI\n\nstruct SettingsView: View {\n    @StateObject private var viewModel = SettingsViewModel()\n\n    var body: some View {\n        Form {\n            // Background services.\n            Section {\n                // Disaster detection.\n                Toggle(isOn: $viewModel.disasterDetectionEnabled) {\n                    Text(\"Disaster Detection\")\n\n                    Text(\"Detects disasters and sends SOS when the app is not in use. This requires location permission. This may increase battery consumption.\")\n                        .font(.caption)\n                        .foregroundColor(.gray)\n                }\n            } header: {\n                Text(\"Background Services\")\n                    .padding(.vertical, 4)\n            } footer: {\n                Text(\"Services might require additional permissions.\")\n                    .padding(.vertical, 4)\n            }\n\n            // Feedback.\n            Section {\n                FeedbackButtonView()\n                // removes the section padding around the feedback row\n                    .listRowInsets(EdgeInsets())\n            } header: {\n                Text(\"Feedback\")\n                    .padding(.vertical, 4)\n            } footer: {\n                Text(\"Your feedback helps us improve Igatha, for everyone.\")\n                    .padding(.vertical, 4)\n            }\n        }\n        .navigationTitle(\"Settings\")\n        // Keep the title small.\n        .navigationBarTitleDisplayMode(.inline)\n    }\n}\n\n#Preview {\n    SettingsView()\n}\n"
  },
  {
    "path": "ios/Igatha.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 77;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t594A117D2CC255BF00592E2A /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 594A117C2CC255BF00592E2A /* CoreLocation.framework */; };\n\t\t595C0AEE2CB1F181009A20E9 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 595C0AED2CB1F181009A20E9 /* CoreBluetooth.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXFileReference section */\n\t\t594A117C2CC255BF00592E2A /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; };\n\t\t595C0AD42CB17946009A20E9 /* Igatha.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Igatha.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t595C0AED2CB1F181009A20E9 /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = System/Library/Frameworks/CoreBluetooth.framework; sourceTree = SDKROOT; };\n/* End PBXFileReference section */\n\n/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */\n\t\t59C19EE72CBFFC9500564F98 /* Exceptions for \"Igatha\" folder in \"Igatha\" target */ = {\n\t\t\tisa = PBXFileSystemSynchronizedBuildFileExceptionSet;\n\t\t\tmembershipExceptions = (\n\t\t\t\tInfo.plist,\n\t\t\t);\n\t\t\ttarget = 595C0AD32CB17946009A20E9 /* Igatha */;\n\t\t};\n/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */\n\n/* Begin PBXFileSystemSynchronizedRootGroup section */\n\t\t595C0AD62CB17946009A20E9 /* Igatha */ = {\n\t\t\tisa = PBXFileSystemSynchronizedRootGroup;\n\t\t\texceptions = (\n\t\t\t\t59C19EE72CBFFC9500564F98 /* Exceptions for \"Igatha\" folder in \"Igatha\" target */,\n\t\t\t);\n\t\t\tpath = Igatha;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXFileSystemSynchronizedRootGroup section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t595C0AD12CB17946009A20E9 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t594A117D2CC255BF00592E2A /* CoreLocation.framework in Frameworks */,\n\t\t\t\t595C0AEE2CB1F181009A20E9 /* CoreBluetooth.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t595C0ACB2CB17946009A20E9 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t595C0AD62CB17946009A20E9 /* Igatha */,\n\t\t\t\t595C0AEA2CB1F179009A20E9 /* Frameworks */,\n\t\t\t\t595C0AD52CB17946009A20E9 /* Products */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t595C0AD52CB17946009A20E9 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t595C0AD42CB17946009A20E9 /* Igatha.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t595C0AEA2CB1F179009A20E9 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t594A117C2CC255BF00592E2A /* CoreLocation.framework */,\n\t\t\t\t595C0AED2CB1F181009A20E9 /* CoreBluetooth.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t595C0AD32CB17946009A20E9 /* Igatha */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 595C0AE22CB17948009A20E9 /* Build configuration list for PBXNativeTarget \"Igatha\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t595C0AD02CB17946009A20E9 /* Sources */,\n\t\t\t\t595C0AD12CB17946009A20E9 /* Frameworks */,\n\t\t\t\t595C0AD22CB17946009A20E9 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t595C0AD62CB17946009A20E9 /* Igatha */,\n\t\t\t);\n\t\t\tname = Igatha;\n\t\t\tpackageProductDependencies = (\n\t\t\t);\n\t\t\tproductName = Igatha;\n\t\t\tproductReference = 595C0AD42CB17946009A20E9 /* Igatha.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t595C0ACC2CB17946009A20E9 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = 1;\n\t\t\t\tLastSwiftUpdateCheck = 1600;\n\t\t\t\tLastUpgradeCheck = 1600;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t595C0AD32CB17946009A20E9 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 16.0;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 595C0ACF2CB17946009A20E9 /* Build configuration list for PBXProject \"Igatha\" */;\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 595C0ACB2CB17946009A20E9;\n\t\t\tminimizedProjectReferenceProxies = 1;\n\t\t\tpreferredProjectObjectVersion = 77;\n\t\t\tproductRefGroup = 595C0AD52CB17946009A20E9 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t595C0AD32CB17946009A20E9 /* Igatha */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t595C0AD22CB17946009A20E9 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t595C0AD02CB17946009A20E9 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin XCBuildConfiguration section */\n\t\t595C0AE02CB17948009A20E9 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu17;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tLOCALIZATION_PREFERS_STRING_CATALOGS = YES;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = \"DEBUG $(inherited)\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t595C0AE12CB17948009A20E9 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu17;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tLOCALIZATION_PREFERS_STRING_CATALOGS = YES;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t595C0AE32CB17948009A20E9 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Igatha/Igatha.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 5;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"Igatha/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = FYA7HX337S;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = Igatha/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = Igatha;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.utilities\";\n\t\t\t\tINFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = \"Used to find SOS signals during emergencies.\";\n\t\t\t\tINFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = \"Used to signal SOS during emergencies.\";\n\t\t\t\tINFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = \"Used to enable the sensors for disaster detection in the background.\";\n\t\t\t\tINFOPLIST_KEY_NSLocationWhenInUseUsageDescription = \"Used to enable the sensors for disaster detection in the foreground.\";\n\t\t\t\tINFOPLIST_KEY_NSMotionUsageDescription = \"Used to detect disasters which affected the user.\";\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.6;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.nizarmah.igatha;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t595C0AE42CB17948009A20E9 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Igatha/Igatha.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 5;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"Igatha/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = FYA7HX337S;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = Igatha/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = Igatha;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.utilities\";\n\t\t\t\tINFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = \"Used to find SOS signals during emergencies.\";\n\t\t\t\tINFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = \"Used to signal SOS during emergencies.\";\n\t\t\t\tINFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = \"Used to enable the sensors for disaster detection in the background.\";\n\t\t\t\tINFOPLIST_KEY_NSLocationWhenInUseUsageDescription = \"Used to enable the sensors for disaster detection in the foreground.\";\n\t\t\t\tINFOPLIST_KEY_NSMotionUsageDescription = \"Used to detect disasters which affected the user.\";\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.6;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.nizarmah.igatha;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t595C0ACF2CB17946009A20E9 /* Build configuration list for PBXProject \"Igatha\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t595C0AE02CB17948009A20E9 /* Debug */,\n\t\t\t\t595C0AE12CB17948009A20E9 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t595C0AE22CB17948009A20E9 /* Build configuration list for PBXNativeTarget \"Igatha\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t595C0AE32CB17948009A20E9 /* Debug */,\n\t\t\t\t595C0AE42CB17948009A20E9 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 595C0ACC2CB17946009A20E9 /* Project object */;\n}\n"
  },
  {
    "path": "ios/Igatha.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Igatha.xcodeproj/xcuserdata/nizarmah.xcuserdatad/xcschemes/xcschememanagement.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>SchemeUserState</key>\n\t<dict>\n\t\t<key>Igatha.xcscheme_^#shared#^_</key>\n\t\t<dict>\n\t\t\t<key>orderHint</key>\n\t\t\t<integer>0</integer>\n\t\t</dict>\n\t</dict>\n</dict>\n</plist>\n"
  }
]