Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,8 @@ android {

// Split APKs per ABI so each download is ~30-45 MB instead of ~90 MB.
// Controlled by --split-per-abi in the Flutter CLI (see build_apk.yml).
splits {
abi {
isEnable = true
reset()
include("arm64-v8a", "armeabi-v7a", "x86_64")
isUniversalApk = false
}
}
// Note: Removed explicit splits block because it conflicts with Flutter's
// internal ndk abiFilters when using --split-per-abi.
}

flutter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import id.flutter.flutter_background_service.FlutterBackgroundServicePlugin
import org.json.JSONObject
import com.roadsos.app.R

// SharedPreferences written by both the tile and Flutter (via MainActivity
// MethodChannel "setCrashMonitorActive") to keep crash-monitor state in sync.
Expand Down
6 changes: 5 additions & 1 deletion lib/config/map_tile_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ class MapTileConfig {
static List<String> get effectiveSubdomains {
final raw = dotenv.maybeGet('MAP_TILE_SUBDOMAINS')?.trim();
if (raw != null && raw.isNotEmpty) {
return raw.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
return raw
.split(',')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
}
final template = effectiveUrlTemplate;
if (template.contains('{s}') &&
Expand Down
4 changes: 3 additions & 1 deletion lib/config/runtime_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ class RuntimeConfig {
case 'SMS_DISPATCH_ANON_KEY':
return const String.fromEnvironment('SMS_DISPATCH_ANON_KEY');
case 'SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH':
return const String.fromEnvironment('SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH');
return const String.fromEnvironment(
'SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH',
);
case 'INDIA_SOS_DISPATCH_URL':
return const String.fromEnvironment('INDIA_SOS_DISPATCH_URL');
case 'INDIA_ERSS_API_URL':
Expand Down
6 changes: 5 additions & 1 deletion lib/database/app_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ Future<void> ensureSupabaseAnonymousSession(SupabaseClient client) async {
return;
}
} catch (e, st) {
appLog.w('Session refresh failed; re-authenticating', error: e, stackTrace: st);
appLog.w(
'Session refresh failed; re-authenticating',
error: e,
stackTrace: st,
);
}
}
await client.auth.signInAnonymously();
Expand Down
28 changes: 21 additions & 7 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ void main() async {
await RuntimeConfig.bootstrap();
} catch (e, st) {
// Non-fatal: all services check for missing config and degrade gracefully.
appLog.w('[boot] RuntimeConfig.bootstrap() failed — proceeding without config', error: e, stackTrace: st);
appLog.w(
'[boot] RuntimeConfig.bootstrap() failed — proceeding without config',
error: e,
stackTrace: st,
);
}

// ← First frame renders here. The loading spinner in _RoadSOSAppState is
Expand Down Expand Up @@ -170,8 +174,9 @@ class _RoadSOSAppState extends ConsumerState<RoadSOSApp>
await Future.wait<void>([
initializeFirstAidRepository(),
initializeFmtcMapCache(),
EmergencyBackgroundService.initialize()
.then((_) => EmergencyBackgroundService.ensureNotificationChannel()),
EmergencyBackgroundService.initialize().then(
(_) => EmergencyBackgroundService.ensureNotificationChannel(),
),
]);

// Phase 4: Kick off remote crash-config fetch (non-blocking).
Expand All @@ -184,7 +189,11 @@ class _RoadSOSAppState extends ConsumerState<RoadSOSApp>
appLog.i('[boot] All services bootstrapped successfully.');
} catch (e, st) {
// Non-fatal: app runs in offline/degraded mode.
appLog.e('[boot] Service bootstrap error — running in degraded mode', error: e, stackTrace: st);
appLog.e(
'[boot] Service bootstrap error — running in degraded mode',
error: e,
stackTrace: st,
);
} finally {
if (mounted) {
setState(() => _servicesReady = true);
Expand Down Expand Up @@ -222,8 +231,9 @@ class _RoadSOSAppState extends ConsumerState<RoadSOSApp>
ref.watch(inactivityCrashDetectorProvider);
ref.watch(sosLocationTrackerProvider);

final sosPhase =
ref.watch(emergencyOrchestratorProvider.select((s) => s.phase));
final sosPhase = ref.watch(
emergencyOrchestratorProvider.select((s) => s.phase),
);
final appLocale = ref.watch(appLocaleProvider);

ref.listen(appLocaleProvider, (_, next) {
Expand Down Expand Up @@ -334,7 +344,11 @@ class _LogoMark extends StatelessWidget {
color: const Color(0xFFE8281A),
borderRadius: BorderRadius.circular(18),
),
child: const Icon(Icons.emergency_share, color: Colors.white, size: 40),
child: const Icon(
Icons.emergency_share,
color: Colors.white,
size: 40,
),
),
const SizedBox(height: 16),
const Text(
Expand Down
19 changes: 7 additions & 12 deletions lib/models/dispatch_channel_status.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
/// Lifecycle of one emergency dispatch channel shown in honest status UI.
enum DispatchChannelLifecycle {
pending,
inProgress,
success,
failed,
skipped,
}
enum DispatchChannelLifecycle { pending, inProgress, success, failed, skipped }

/// One row in the dispatch confirmation list (SMS, mesh, cloud, etc.).
class DispatchChannelRow {
final String id;
final String title;
final DispatchChannelLifecycle lifecycle;

/// Short line for accessibility and panic readability (WCAG-minded contrast in UI).
final String detail;

Expand All @@ -35,11 +30,11 @@ class DispatchChannelRow {
}

Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'lifecycle': lifecycle.name,
'detail': detail,
};
'id': id,
'title': title,
'lifecycle': lifecycle.name,
'detail': detail,
};

DispatchChannelRow copyWith({
DispatchChannelLifecycle? lifecycle,
Expand Down
1 change: 1 addition & 0 deletions lib/models/facility.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Facility {
final double longitude;
final String? contactNumber;
final String? capabilities;

/// gov_nhm | gov_ayushman | osm | merged
final String? dataSource;

Expand Down
26 changes: 13 additions & 13 deletions lib/models/sos_activity_record.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,19 @@ class SosActivityRecord {
}

Map<String, dynamic> toJson() => {
'incident_id': incidentId,
'completed_at': completedAtUtc.toIso8601String(),
'latitude': latitude,
'longitude': longitude,
'accuracy_m': accuracyM,
'location_source': locationSource,
'triage_severity': triageSeverity,
'triage_source': triageSourceName,
'required_services': requiredServices,
'channels': channels.map((e) => e.toJson()).toList(),
'sync_status': syncStatusLine,
'is_bystander': isBystander,
};
'incident_id': incidentId,
'completed_at': completedAtUtc.toIso8601String(),
'latitude': latitude,
'longitude': longitude,
'accuracy_m': accuracyM,
'location_source': locationSource,
'triage_severity': triageSeverity,
'triage_source': triageSourceName,
'required_services': requiredServices,
'channels': channels.map((e) => e.toJson()).toList(),
'sync_status': syncStatusLine,
'is_bystander': isBystander,
};

String formattedGpsIndia() =>
'${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)} '
Expand Down
22 changes: 12 additions & 10 deletions lib/services/agent_health_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ class AgentHealthSnapshot {
AgentReadiness? sms,
AgentReadiness? ble,
}) => AgentHealthSnapshot(
gemmaCloud: gemmaCloud ?? this.gemmaCloud,
gemmaOnDevice: gemmaOnDevice ?? this.gemmaOnDevice,
gps: gps ?? this.gps,
sms: sms ?? this.sms,
ble: ble ?? this.ble,
checkedAt: DateTime.now(),
);
gemmaCloud: gemmaCloud ?? this.gemmaCloud,
gemmaOnDevice: gemmaOnDevice ?? this.gemmaOnDevice,
gps: gps ?? this.gps,
sms: sms ?? this.sms,
ble: ble ?? this.ble,
checkedAt: DateTime.now(),
);
}

class AgentHealthService {
Expand Down Expand Up @@ -135,9 +135,9 @@ class AgentHealthService {
Future<AgentReadiness> _checkGemmaCloud() async {
final connectivity = _ref.read(connectivityServiceProvider);
return switch (connectivity.currentQuality) {
NetworkQuality.wifi => AgentReadiness.ready,
NetworkQuality.wifi => AgentReadiness.ready,
NetworkQuality.cellular => AgentReadiness.ready,
NetworkQuality.none => AgentReadiness.unavailable,
NetworkQuality.none => AgentReadiness.unavailable,
};
}

Expand Down Expand Up @@ -168,7 +168,9 @@ class AgentHealthService {
// Primary: server relay (Twilio / Edge) — no Android SEND_SMS required.
final relayUrl = dotenv.env['SMS_DISPATCH_URL']?.trim() ?? '';
final relayKey = dotenv.env['SMS_DISPATCH_ANON_KEY']?.trim() ?? '';
if (relayUrl.isNotEmpty && relayKey.isNotEmpty) return AgentReadiness.ready;
if (relayUrl.isNotEmpty && relayKey.isNotEmpty) {
return AgentReadiness.ready;
}

// Fallback: open SMS app intent (no permission). If relay isn't configured,
// we mark this as degraded (still usable, but requires user interaction).
Expand Down
Loading
Loading