Spaces:
Running
Running
| import 'dart:async'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/services.dart'; | |
| import 'package:intl/intl.dart'; | |
| import '../services/vector_service.dart'; | |
| import '../services/storage_service.dart'; | |
| import '../services/telemetry_service.dart'; | |
| import 'app_lock_screen.dart'; | |
| import 'vpn_shield_screen.dart'; | |
| class DataShieldScreen extends StatefulWidget { | |
| const DataShieldScreen({super.key}); | |
| State<DataShieldScreen> createState() => _DataShieldScreenState(); | |
| } | |
| class _DataShieldScreenState extends State<DataShieldScreen> { | |
| int _embeddingCount = 0; | |
| List<PrivacyStory> stories = []; | |
| List<AppDataUsage> dataUsage = []; | |
| List<SensorAccess> activeSensors = []; | |
| bool isLoading = true; | |
| bool hasPermission = false; | |
| String selectedLanguage = 'english'; // english or hindi | |
| DateTime lastScan = DateTime.now(); | |
| Timer? _sensorTimer; | |
| void initState() { | |
| super.initState(); | |
| _loadEmbeddingCount(); | |
| _fetchTelemetry(); | |
| _sensorTimer = Timer.periodic(const Duration(seconds: 60), (_) { | |
| _fetchActiveSensors(); | |
| }); | |
| } | |
| void dispose() { | |
| _sensorTimer?.cancel(); | |
| super.dispose(); | |
| } | |
| Future<void> _loadEmbeddingCount() async { | |
| if (!VectorService.isReady) return; | |
| final db = await StorageService().database; | |
| final count = await VectorService().embeddingCount(db); | |
| if (mounted) setState(() => _embeddingCount = count); | |
| } | |
| Future<void> _fetchTelemetry() async { | |
| if (!mounted) return; | |
| setState(() => isLoading = true); | |
| final perm = await TelemetryService.hasUsagePermission(); | |
| final st = await TelemetryService.getPrivacyStories(); | |
| final dUsage = await TelemetryService.getDataUsage(); | |
| final sensors = await TelemetryService.getActiveSensors(); | |
| if (mounted) { | |
| setState(() { | |
| hasPermission = perm; | |
| stories = st; | |
| dataUsage = dUsage; | |
| activeSensors = sensors; | |
| lastScan = DateTime.now(); | |
| isLoading = false; | |
| }); | |
| } | |
| } | |
| Future<void> _fetchActiveSensors() async { | |
| final sensors = await TelemetryService.getActiveSensors(); | |
| if (mounted) { | |
| setState(() { | |
| activeSensors = sensors; | |
| }); | |
| } | |
| } | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| backgroundColor: const Color(0xFF0F1117), | |
| appBar: AppBar( | |
| title: const Text( | |
| 'Data Shield', | |
| style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white), | |
| ), | |
| backgroundColor: const Color(0xFF1A1D27), | |
| foregroundColor: Colors.white, | |
| elevation: 0, | |
| actions: [ | |
| // Language Toggle | |
| Padding( | |
| padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), | |
| child: Container( | |
| decoration: BoxDecoration( | |
| color: const Color(0xFF2D3145), | |
| borderRadius: BorderRadius.circular(20), | |
| ), | |
| child: Row( | |
| children: [ | |
| _langButton('EN', 'english'), | |
| _langButton('हिं', 'hindi'), | |
| ], | |
| ), | |
| ), | |
| ), | |
| IconButton( | |
| icon: const Icon(Icons.refresh, color: Colors.white70), | |
| tooltip: 'Refresh', | |
| onPressed: () { | |
| _fetchTelemetry(); | |
| }, | |
| ), | |
| IconButton( | |
| icon: const Icon(Icons.shield, color: Colors.white70), | |
| tooltip: 'DNS Shield', | |
| onPressed: () => Navigator.push( | |
| context, | |
| MaterialPageRoute(builder: (_) => const VpnShieldScreen()), | |
| ), | |
| ), | |
| IconButton( | |
| icon: const Icon(Icons.lock_outline, color: Colors.white70), | |
| tooltip: 'App Lock', | |
| onPressed: () => Navigator.push( | |
| context, | |
| MaterialPageRoute(builder: (_) => const AppLockScreen()), | |
| ), | |
| ), | |
| ], | |
| ), | |
| floatingActionButton: FloatingActionButton.extended( | |
| onPressed: isLoading ? null : _fetchTelemetry, | |
| backgroundColor: Colors.blueAccent, | |
| icon: isLoading | |
| ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) | |
| : const Icon(Icons.search, color: Colors.white), | |
| label: const Text('Scan Now', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), | |
| ), | |
| body: SingleChildScrollView( | |
| padding: const EdgeInsets.all(16.0), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| if (!hasPermission && !isLoading) _buildPermissionBanner(), | |
| const SizedBox(height: 16), | |
| _buildLiveSensorStatus(), | |
| const SizedBox(height: 24), | |
| _sectionHeader('📊 Privacy Stories'), | |
| const SizedBox(height: 8), | |
| if (isLoading && stories.isEmpty) | |
| const Center(child: Padding(padding: EdgeInsets.all(20), child: CircularProgressIndicator())) | |
| else | |
| ...stories.map(_buildStoryCard).toList(), | |
| const SizedBox(height: 24), | |
| if (dataUsage.isNotEmpty) ...[ | |
| _sectionHeader('📤 Data Sent (last 24h)'), | |
| const SizedBox(height: 8), | |
| _buildDataUsageSection(), | |
| const SizedBox(height: 24), | |
| ], | |
| _sectionHeader('🛡️ System Status'), | |
| const SizedBox(height: 8), | |
| _buildSystemStatusCard(), | |
| const SizedBox(height: 80), // Fab spacing | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _langButton(String label, String langVal) { | |
| final active = selectedLanguage == langVal; | |
| return GestureDetector( | |
| onTap: () => setState(() => selectedLanguage = langVal), | |
| child: Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), | |
| decoration: BoxDecoration( | |
| color: active ? Colors.blueAccent : Colors.transparent, | |
| borderRadius: BorderRadius.circular(20), | |
| ), | |
| child: Text( | |
| label, | |
| style: TextStyle( | |
| color: active ? Colors.white : Colors.white54, | |
| fontWeight: active ? FontWeight.bold : FontWeight.normal, | |
| fontSize: 13, | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildPermissionBanner() { | |
| return Container( | |
| padding: const EdgeInsets.all(16), | |
| decoration: BoxDecoration( | |
| color: Colors.amber.shade900.withValues(alpha: 0.2), | |
| borderRadius: BorderRadius.circular(12), | |
| border: Border.all(color: Colors.amber.shade700), | |
| ), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Row( | |
| children: [ | |
| Icon(Icons.warning_amber_rounded, color: Colors.amber.shade300), | |
| const SizedBox(width: 8), | |
| const Expanded( | |
| child: Text( | |
| 'Grant Usage Access for full insights', | |
| style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 8), | |
| const Text( | |
| 'Premithius reads app usage locally. Nothing leaves your device.', | |
| style: TextStyle(color: Colors.white70, fontSize: 13), | |
| ), | |
| const SizedBox(height: 12), | |
| ElevatedButton( | |
| onPressed: () async { | |
| const platform = MethodChannel('flutter/platform_channel'); | |
| try { | |
| await platform.invokeMethod('openSettingsIntent', 'android.settings.USAGE_ACCESS_SETTINGS'); | |
| } catch (_) {} | |
| }, | |
| style: ElevatedButton.styleFrom( | |
| backgroundColor: Colors.amber.shade700, | |
| foregroundColor: Colors.white, | |
| ), | |
| child: const Text('Open Settings'), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildLiveSensorStatus() { | |
| final activeMic = activeSensors.where((s) => s.sensor == 'microphone').toList(); | |
| final activeCam = activeSensors.where((s) => s.sensor == 'camera').toList(); | |
| return Row( | |
| children: [ | |
| Expanded(child: _buildSensorCard('Microphone', Icons.mic, activeMic)), | |
| const SizedBox(width: 12), | |
| Expanded(child: _buildSensorCard('Camera', Icons.videocam, activeCam)), | |
| ], | |
| ); | |
| } | |
| Widget _buildSensorCard(String title, IconData icon, List<SensorAccess> activeList) { | |
| final bool isActive = activeList.isNotEmpty; | |
| final Color bgColor = isActive ? Colors.red.shade900 : Colors.green.shade900.withValues(alpha: 0.5); | |
| final Color borderColor = isActive ? Colors.red.shade400 : Colors.green.shade700; | |
| return Container( | |
| padding: const EdgeInsets.all(12), | |
| decoration: BoxDecoration( | |
| color: bgColor, | |
| borderRadius: BorderRadius.circular(12), | |
| border: Border.all(color: borderColor, width: 2), | |
| ), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Row( | |
| children: [ | |
| Icon(icon, color: Colors.white, size: 20), | |
| const SizedBox(width: 8), | |
| Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), | |
| if (isActive) ...[ | |
| const Spacer(), | |
| Container( | |
| width: 8, height: 8, | |
| decoration: const BoxDecoration( | |
| color: Colors.white, | |
| shape: BoxShape.circle, | |
| ), | |
| ), | |
| ] | |
| ], | |
| ), | |
| const SizedBox(height: 8), | |
| Text( | |
| isActive ? "LIVE — ${activeList.first.appName}" : "No apps listening", | |
| style: const TextStyle(color: Colors.white70, fontSize: 13), | |
| maxLines: 1, overflow: TextOverflow.ellipsis, | |
| ) | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildStoryCard(PrivacyStory story) { | |
| Color badgeColor; | |
| switch(story.severity) { | |
| case 'CRITICAL': badgeColor = Colors.red; break; | |
| case 'HIGH': badgeColor = Colors.orange; break; | |
| case 'MEDIUM': badgeColor = Colors.amber; break; | |
| case 'SAFE': badgeColor = Colors.green; break; | |
| default: badgeColor = Colors.grey; | |
| } | |
| final text = selectedLanguage == 'hindi' ? story.hindi : story.english; | |
| return Container( | |
| margin: const EdgeInsets.only(bottom: 12), | |
| padding: const EdgeInsets.all(16), | |
| decoration: BoxDecoration( | |
| color: const Color(0xFF1A1D27), | |
| borderRadius: BorderRadius.circular(12), | |
| border: Border.all(color: const Color(0xFF2D3145)), | |
| boxShadow: [ | |
| BoxShadow( | |
| color: Colors.black.withValues(alpha: 0.2), | |
| blurRadius: 8, | |
| offset: const Offset(0, 4), | |
| ), | |
| ], | |
| ), | |
| child: Row( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text(story.emoji, style: const TextStyle(fontSize: 32)), | |
| const SizedBox(width: 16), | |
| Expanded( | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), | |
| decoration: BoxDecoration( | |
| color: badgeColor.withValues(alpha: 0.2), | |
| borderRadius: BorderRadius.circular(4), | |
| border: Border.all(color: badgeColor), | |
| ), | |
| child: Text(story.severity, style: TextStyle(color: badgeColor, fontSize: 10, fontWeight: FontWeight.bold)), | |
| ), | |
| const SizedBox(height: 8), | |
| Text( | |
| text, | |
| style: const TextStyle( | |
| color: Colors.white, | |
| height: 1.4, | |
| fontSize: 14, | |
| ), | |
| ), | |
| if (story.packageName.isNotEmpty && story.severity != 'SAFE') ...[ | |
| const SizedBox(height: 12), | |
| SizedBox( | |
| height: 32, | |
| child: OutlinedButton( | |
| style: OutlinedButton.styleFrom( | |
| foregroundColor: Colors.white70, | |
| side: const BorderSide(color: Colors.white24), | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | |
| ), | |
| onPressed: () async { | |
| const channel = MethodChannel('in.inmodel.premithius/accessibility'); | |
| try { | |
| await channel.invokeMethod('launchApp', {'packageName': story.packageName}); | |
| } catch (_) {} | |
| }, | |
| child: const Text('Open App', style: TextStyle(fontSize: 12)), | |
| ), | |
| ) | |
| ] | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildDataUsageSection() { | |
| return Container( | |
| padding: const EdgeInsets.all(16), | |
| decoration: BoxDecoration( | |
| color: const Color(0xFF1A1D27), | |
| borderRadius: BorderRadius.circular(12), | |
| border: Border.all(color: const Color(0xFF2D3145)), | |
| ), | |
| child: Column( | |
| children: dataUsage.take(5).map((usage) { | |
| final maxMb = dataUsage.first.mbSent; | |
| final pct = maxMb > 0 ? (usage.mbSent / maxMb) : 0.0; | |
| return Padding( | |
| padding: const EdgeInsets.only(bottom: 12.0), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| children: [ | |
| Text(usage.appName, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)), | |
| Text("${usage.mbSent.toStringAsFixed(1)} MB", style: const TextStyle(color: Colors.white70, fontSize: 12)), | |
| ], | |
| ), | |
| const SizedBox(height: 6), | |
| LinearProgressIndicator( | |
| value: pct, | |
| backgroundColor: const Color(0xFF2D3145), | |
| color: usage.mbSent > 100 ? Colors.redAccent : Colors.blueAccent, | |
| minHeight: 6, | |
| borderRadius: BorderRadius.circular(4), | |
| ), | |
| ], | |
| ), | |
| ); | |
| }).toList(), | |
| ), | |
| ); | |
| } | |
| Widget _sectionHeader(String title) { | |
| return Text( | |
| title, | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontSize: 18, | |
| fontWeight: FontWeight.bold, | |
| letterSpacing: 0.3, | |
| ), | |
| ); | |
| } | |
| Widget _buildSystemStatusCard() { | |
| final bool vecReady = VectorService.isReady; | |
| final String vecVersion = VectorService.vecVersion; | |
| return Container( | |
| padding: const EdgeInsets.all(16), | |
| decoration: BoxDecoration( | |
| color: const Color(0xFF1A1D27), | |
| borderRadius: BorderRadius.circular(16), | |
| border: Border.all( | |
| color: const Color(0xFF2D3145), | |
| width: 1, | |
| ), | |
| boxShadow: [ | |
| BoxShadow( | |
| color: Colors.black.withValues(alpha: 0.3), | |
| blurRadius: 12, | |
| offset: const Offset(0, 4), | |
| ), | |
| ], | |
| ), | |
| child: Column( | |
| children: [ | |
| _statusRow( | |
| 'Layer 1 — India Threat Pack', | |
| true, | |
| 'ACTIVE', | |
| ), | |
| const Divider(color: Color(0xFF2D3145), height: 16), | |
| _statusRow( | |
| 'Layer 2 — Vector Search', | |
| vecReady, | |
| vecReady ? 'ACTIVE' : 'DISABLED', | |
| ), | |
| const Divider(color: Color(0xFF2D3145), height: 16), | |
| _statusRow( | |
| 'Layer 3 — Random Forest', | |
| true, | |
| 'ACTIVE', | |
| ), | |
| const Divider(color: Color(0xFF2D3145), height: 16), | |
| _statusRow( | |
| 'Layer 4 — MobileBERT', | |
| true, | |
| 'ACTIVE', | |
| ), | |
| const Divider(color: Color(0xFF2D3145), height: 16), | |
| _infoRow( | |
| Icons.extension, | |
| 'sqlite-vec version', | |
| vecReady ? vecVersion : 'not loaded', | |
| vecReady ? Colors.blueAccent : Colors.grey, | |
| ), | |
| const Divider(color: Color(0xFF2D3145), height: 16), | |
| _infoRow( | |
| Icons.storage, | |
| 'Embeddings in store', | |
| vecReady ? '$_embeddingCount' : '—', | |
| vecReady ? Colors.blueAccent : Colors.grey, | |
| ), | |
| const Divider(color: Color(0xFF2D3145), height: 16), | |
| _infoRow( | |
| Icons.bar_chart, | |
| 'Usage Stats Permission', | |
| hasPermission ? 'GRANTED' : 'NEEDED', | |
| hasPermission ? Colors.green : Colors.amber, | |
| ), | |
| const Divider(color: Color(0xFF2D3145), height: 16), | |
| _infoRow( | |
| Icons.article, | |
| 'Privacy Stories', | |
| '${stories.length} generated', | |
| Colors.blueAccent, | |
| ), | |
| const Divider(color: Color(0xFF2D3145), height: 16), | |
| _infoRow( | |
| Icons.schedule, | |
| 'Last scan', | |
| DateFormat('HH:mm:ss').format(lastScan), | |
| Colors.white70, | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _statusRow(String label, bool isActive, String statusText) { | |
| final Color statusColor = | |
| isActive ? const Color(0xFF00E676) : Colors.grey; | |
| final Color pillBg = | |
| isActive ? const Color(0xFF00E676).withValues(alpha: 0.15) : Colors.grey.withValues(alpha: 0.15); | |
| return Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| children: [ | |
| Expanded( | |
| child: Text( | |
| label, | |
| style: const TextStyle( | |
| color: Colors.white70, | |
| fontSize: 14, | |
| ), | |
| ), | |
| ), | |
| Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), | |
| decoration: BoxDecoration( | |
| color: pillBg, | |
| borderRadius: BorderRadius.circular(20), | |
| border: Border.all(color: statusColor.withValues(alpha: 0.5)), | |
| ), | |
| child: Text( | |
| statusText, | |
| style: TextStyle( | |
| color: statusColor, | |
| fontSize: 12, | |
| fontWeight: FontWeight.bold, | |
| letterSpacing: 0.5, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| Widget _infoRow(IconData icon, String label, String value, Color valueColor) { | |
| return Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| children: [ | |
| Row( | |
| children: [ | |
| Icon(icon, color: Colors.white54, size: 16), | |
| const SizedBox(width: 8), | |
| Text( | |
| label, | |
| style: const TextStyle(color: Colors.white70, fontSize: 14), | |
| ), | |
| ], | |
| ), | |
| Text( | |
| value, | |
| style: TextStyle( | |
| color: valueColor, | |
| fontSize: 14, | |
| fontWeight: FontWeight.w600, | |
| fontFamily: 'monospace', | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |