From 6fa36a14d9fb6a3220c2d81cb0318df4fec4b1da Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 22 Feb 2026 02:27:03 +0200 Subject: [PATCH 01/46] Add Yara Malware scan --- assets/translations/en.json | 31 ++ lib/pages/home.dart | 4 +- lib/pages/security_settings.dart | 344 +++++++++++++++++++ lib/providers/apps_provider.dart | 28 ++ lib/security/security_settings_provider.dart | 146 ++++++++ lib/security/yara_scanner.dart | 317 +++++++++++++++++ pubspec.yaml | 2 + 7 files changed, 870 insertions(+), 2 deletions(-) create mode 100644 lib/pages/security_settings.dart create mode 100644 lib/security/security_settings_provider.dart create mode 100644 lib/security/yara_scanner.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 3e3117be5..be2090e78 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -28,6 +28,37 @@ "githubStarredRepos": "GitHub starred repositories", "uname": "Username", "wrongArgNum": "Wrong number of arguments provided", + "yaraMalwareScanner": "YARA Malware Scanner", + "yaraScannerDescription": "Industry-standard malware detection using YARA rules for comprehensive protection against viruses, trojans, and other threats.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} is track-only", "source": "Source", "app": "App", diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 255a0020d..ae409b853 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -11,7 +11,7 @@ import 'package:updatium/custom_errors.dart'; import 'package:updatium/pages/add_app.dart'; import 'package:updatium/pages/apps.dart'; import 'package:updatium/pages/security_disclaimer.dart'; -import 'package:updatium/pages/settings.dart'; +import 'package:updatium/pages/security_settings.dart'; import 'package:updatium/providers/apps_provider.dart'; import 'package:updatium/providers/settings_provider.dart'; import 'package:updatium/providers/source_provider.dart'; @@ -55,7 +55,7 @@ class _HomePageState extends State with TickerProviderStateMixin { Icons.add_circle, AddAppPage(key: GlobalKey()), ), - NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()), + NavigationPageItem(tr('settings'), Icons.settings, const SecuritySettingsPage()), ]; @override diff --git a/lib/pages/security_settings.dart b/lib/pages/security_settings.dart new file mode 100644 index 000000000..2d7cdc3c8 --- /dev/null +++ b/lib/pages/security_settings.dart @@ -0,0 +1,344 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:updatium/security/security_settings_provider.dart'; + +class SecuritySettingsPage extends StatefulWidget { + const SecuritySettingsPage({super.key}); + + @override + State createState() => _SecuritySettingsPageState(); +} + +class _SecuritySettingsPageState extends State { + late SecuritySettingsProvider _securityProvider; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _initializeSecurityProvider(); + } + + Future _initializeSecurityProvider() async { + _securityProvider = await SecuritySettingsProvider.create(); + await _securityProvider.initialize(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(tr('securitySettings')), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + _buildHeader(), + const SizedBox(height: 16), + _buildAutoScanSection(), + const SizedBox(height: 16), + _buildAutoUpdateSection(), + const SizedBox(height: 16), + _buildThreatLevelSection(), + const SizedBox(height: 16), + _buildQuarantineSection(), + const SizedBox(height: 16), + _buildUpdateSection(), + ], + ), + ); + } + + Widget _buildHeader() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.security, + color: Theme.of(context).colorScheme.primary, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('yaraMalwareScanner'), + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + tr('yaraScannerDescription'), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildAutoScanSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('autoScanSettings'), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + SwitchListTile( + title: Text(tr('enableAutoScan')), + subtitle: Text(tr('enableAutoScanDescription')), + value: _securityProvider.getAutoScanEnabled(), + onChanged: (value) async { + await _securityProvider.setAutoScanEnabled(value); + setState(() {}); + }, + ), + ], + ), + ), + ); + } + + Widget _buildAutoUpdateSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('autoUpdateSettings'), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + SwitchListTile( + title: Text(tr('enableAutoUpdate')), + subtitle: Text(tr('enableAutoUpdateDescription')), + value: _securityProvider.getAutoUpdateEnabled(), + onChanged: (value) async { + await _securityProvider.setAutoUpdateEnabled(value); + setState(() {}); + }, + ), + const SizedBox(height: 8), + ListTile( + title: Text(tr('updateInterval')), + subtitle: Text(tr('updateIntervalDescription')), + trailing: DropdownButton( + value: _securityProvider.getUpdateInterval(), + items: [1, 6, 12, 24, 48, 72].map((hours) { + return DropdownMenuItem( + value: hours, + child: Text('$hours ${tr('hours')}'), + ); + }).toList(), + onChanged: (value) async { + if (value != null) { + await _securityProvider.setUpdateInterval(value); + setState(() {}); + } + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildThreatLevelSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('threatLevelFilter'), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + tr('threatLevelFilterDescription'), + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 8), + RadioListTile( + title: Text(tr('level1')), + subtitle: Text(tr('lowThreat')), + value: 1, + groupValue: _securityProvider.getThreatLevelFilter(), + onChanged: (value) async { + if (value != null) { + await _securityProvider.setThreatLevelFilter(value); + setState(() {}); + } + }, + ), + RadioListTile( + title: Text(tr('level2')), + subtitle: Text(tr('mediumThreat')), + value: 2, + groupValue: _securityProvider.getThreatLevelFilter(), + onChanged: (value) async { + if (value != null) { + await _securityProvider.setThreatLevelFilter(value); + setState(() {}); + } + }, + ), + RadioListTile( + title: Text(tr('level3')), + subtitle: Text(tr('highThreat')), + value: 3, + groupValue: _securityProvider.getThreatLevelFilter(), + onChanged: (value) async { + if (value != null) { + await _securityProvider.setThreatLevelFilter(value); + setState(() {}); + } + }, + ), + ], + ), + ), + ); + } + + Widget _buildQuarantineSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('quarantineSettings'), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + SwitchListTile( + title: Text(tr('quarantineInfected')), + subtitle: Text(tr('quarantineInfectedDescription')), + value: _securityProvider.getQuarantineInfected(), + onChanged: (value) async { + await _securityProvider.setQuarantineInfected(value); + setState(() {}); + }, + ), + const SizedBox(height: 8), + ListTile( + title: Text(tr('viewQuarantine')), + subtitle: Text(tr('viewQuarantineDescription')), + trailing: const Icon(Icons.folder), + onTap: () { + // TODO: Navigate to quarantine view + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(tr('quarantineViewComingSoon'))), + ); + }, + ), + ], + ), + ), + ); + } + + Widget _buildUpdateSection() { + final lastUpdate = _securityProvider.getLastUpdate(); + final rulesVersion = _securityProvider.getRulesVersion(); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('databaseInformation'), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + ListTile( + title: Text(tr('lastUpdate')), + subtitle: lastUpdate != null + ? Text(lastUpdate.toString()) + : Text(tr('never')), + trailing: const Icon(Icons.update), + ), + ListTile( + title: Text(tr('rulesVersion')), + subtitle: Text(rulesVersion ?? tr('unknown')), + trailing: const Icon(Icons.code), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _isLoading ? null : _updateRules, + icon: _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.download), + label: Text(_isLoading ? tr('updating') : tr('updateNow')), + ), + ), + ], + ), + ), + ); + } + + Future _updateRules() async { + setState(() { + _isLoading = true; + }); + + try { + await _securityProvider.updateRules(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(tr('rulesUpdatedSuccessfully'))), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(tr('rulesUpdateFailed'))), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + void dispose() { + _securityProvider.dispose(); + super.dispose(); + } +} diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 4ca98fa24..204e2b293 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -43,6 +43,7 @@ import 'package:flutter_archive/flutter_archive.dart'; import 'package:share_plus/share_plus.dart'; import 'package:shared_storage/shared_storage.dart' as saf; import 'package:shizuku_apk_installer/shizuku_apk_installer.dart'; +import 'package:updatium/security/security_settings_provider.dart'; final pm = AndroidPackageManager(); final packageInfoFlags = PackageInfoFlags({PMFlag.getSigningCertificates}); @@ -945,6 +946,27 @@ class AppsProvider with ChangeNotifier { return somethingInstalled; } + /// Scan APK for malware before installation + Future _scanAPKForMalware(String apkPath) async { + try { + final securityProvider = await SecuritySettingsProvider.create(); + await securityProvider.initialize(); + final scanResult = await securityProvider.scanAPK(apkPath); + + if (scanResult.isInfected) { + logs.add('Security scan detected malware in APK: ${scanResult.matches.map((m) => m.ruleName).join(', ')}'); + return false; // Block installation + } + + return true; // Safe to install + } catch (e) { + logs.add('Security scan failed: $e'); + return true; // Allow installation on scan failure + } finally { + // Security provider will be disposed by caller if needed + } + } + Future installApk( DownloadedApk file, BuildContext? firstTimeWithContext, { @@ -981,6 +1003,12 @@ class AppsProvider with ChangeNotifier { } } PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); + + // Security scan before installation + if (!(await _scanAPKForMalware(file.file.path))) { + throw UpdatiumError('Security scan detected malware. Installation blocked for safety.'); + } + logs.add( 'Installing "${newInfo.packageName}" version "${newInfo.versionName}" versionCode "${newInfo.versionCode}"${appInfo != null ? ' (from existing version "${appInfo.versionName}" versionCode "${appInfo.versionCode}")' : ''}', ); diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart new file mode 100644 index 000000000..086b7514c --- /dev/null +++ b/lib/security/security_settings_provider.dart @@ -0,0 +1,146 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:path/path.dart' as path; +import 'dart:convert'; +import 'package:updatium/security/yara_scanner.dart'; + +/// Security Settings Provider +class SecuritySettingsProvider { + static const String _keyAutoScan = 'yara_auto_scan'; + static const String _keyAutoUpdate = 'yara_auto_update'; + static const String _keyUpdateInterval = 'yara_update_interval'; + static const String _keyThreatLevel = 'yara_threat_level'; + static const String _keyQuarantineInfected = 'yara_quarantine_infected'; + static const String _keyLastUpdate = 'yara_last_update'; + static const String _keyRulesVersion = 'yara_rules_version'; + + final SharedPreferences _prefs; + late YARAConfig _config; + late YARAScanner _scanner; + + SecuritySettingsProvider._(this._prefs) { + _config = YARAConfig( + rulesDirectory: _getRulesDirectory(), + updateInterval: Duration(hours: getUpdateInterval()), + enableAutoUpdate: getAutoUpdateEnabled(), + ); + _scanner = YARAScanner(_config); + } + + static Future create() async { + final prefs = await SharedPreferences.getInstance(); + return SecuritySettingsProvider._(prefs); + } + + /// Get the directory for YARA rules + String _getRulesDirectory() { + final appDir = Directory.systemTemp.parent; + final rulesDir = Directory('${appDir.path}/yara_rules'); + return rulesDir.path; + } + + // Auto Scan Settings + bool getAutoScanEnabled() => _prefs.getBool(_keyAutoScan) ?? true; + Future setAutoScanEnabled(bool enabled) => _prefs.setBool(_keyAutoScan, enabled); + + // Auto Update Settings + bool getAutoUpdateEnabled() => _prefs.getBool(_keyAutoUpdate) ?? true; + Future setAutoUpdateEnabled(bool enabled) => _prefs.setBool(_keyAutoUpdate, enabled); + + // Update Interval Settings + int getUpdateInterval() => _prefs.getInt(_keyUpdateInterval) ?? 24; // hours + Future setUpdateInterval(int hours) => _prefs.setInt(_keyUpdateInterval, hours); + + // Threat Level Filter + int getThreatLevelFilter() => _prefs.getInt(_keyThreatLevel) ?? 1; + Future setThreatLevelFilter(int level) => _prefs.setInt(_keyThreatLevel, level); + + // Quarantine Settings + bool getQuarantineInfected() => _prefs.getBool(_keyQuarantineInfected) ?? true; + Future setQuarantineInfected(bool enabled) => _prefs.setBool(_keyQuarantineInfected, enabled); + + // Last Update Tracking + DateTime? getLastUpdate() { + final timestamp = _prefs.getInt(_keyLastUpdate); + return timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp) : null; + } + + Future setLastUpdate(DateTime update) => _prefs.setInt(_keyLastUpdate, update.millisecondsSinceEpoch); + + // Rules Version Tracking + String? getRulesVersion() => _prefs.getString(_keyRulesVersion); + Future setRulesVersion(String version) => _prefs.setString(_keyRulesVersion, version); + + /// Initialize the security scanner + Future initialize() async { + await _scanner.initialize(); + } + + /// Scan an APK file + Future scanAPK(String apkPath) async { + if (!getAutoScanEnabled()) { + return YARAScanResult.error(apkPath, 'Auto-scan is disabled'); + } + + final result = await _scanner.scanFile(apkPath); + + // Handle quarantine if enabled + if (result.isInfected && getQuarantineInfected()) { + await _quarantineFile(apkPath, result); + } + + return result; + } + + /// Move infected file to quarantine + Future _quarantineFile(String filePath, YARAScanResult result) async { + try { + final quarantineDir = Directory('${_getRulesDirectory()}/quarantine'); + if (!await quarantineDir.exists()) { + await quarantineDir.create(recursive: true); + } + + final originalFile = File(filePath); + final fileName = path.basename(filePath); + final timestamp = DateTime.now().millisecondsSinceEpoch; + final quarantinedPath = '${quarantineDir.path}/$timestamp-$fileName'; + + await originalFile.rename(quarantinedPath); + + // Save scan report + final reportPath = '${quarantineDir.path}/$timestamp-$fileName-report.json'; + final report = { + 'originalPath': filePath, + 'quarantinedPath': quarantinedPath, + 'scanTime': result.scanTime.toIso8601String(), + 'matches': result.matches.map((m) => m.toJson()).toList(), + }; + + await File(reportPath).writeAsString(jsonEncode(report)); + + print('File quarantined: $quarantinedPath'); + } catch (e) { + print('Error quarantining file: $e'); + } + } + + /// Update YARA rules + Future updateRules() async { + try { + await _scanner.updateRules(); + await setLastUpdate(DateTime.now()); + await setRulesVersion('latest-${DateTime.now().millisecondsSinceEpoch}'); + } catch (e) { + print('Error updating rules: $e'); + } + } + + /// Get scanner instance + YARAScanner get scanner => _scanner; + + /// Dispose resources + void dispose() { + _scanner.dispose(); + } +} diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart new file mode 100644 index 000000000..295634a4f --- /dev/null +++ b/lib/security/yara_scanner.dart @@ -0,0 +1,317 @@ +import 'dart:io'; +import 'dart:convert'; +import 'dart:async'; +import 'package:path/path.dart' as path; +import 'package:crypto/crypto.dart'; +import 'package:http/http.dart' as http; + +/// YARA Scanner Configuration +class YARAConfig { + final String rulesDirectory; + final Duration updateInterval; + final bool enableAutoUpdate; + final List ruleSources; + + const YARAConfig({ + required this.rulesDirectory, + this.updateInterval = const Duration(hours: 24), + this.enableAutoUpdate = true, + this.ruleSources = const [ + 'https://raw.githubusercontent.com/Yara-Rules/rules/master/index.yar', + 'https://raw.githubusercontent.com/Yara-Rules/rules/master/malware/MALW_YaraRule_APT.yar', + 'https://raw.githubusercontent.com/Yara-Rules/rules/master/malware/MALW_YaraRule_Mobile.yar', + ], + }); +} + +/// YARA Scan Result +class YARAScanResult { + final bool isInfected; + final List matches; + final String filePath; + final DateTime scanTime; + final String? error; + + const YARAScanResult({ + required this.isInfected, + required this.matches, + required this.filePath, + required this.scanTime, + this.error, + }); + + factory YARAScanResult.error(String filePath, String error) { + return YARAScanResult( + isInfected: false, + matches: [], + filePath: filePath, + scanTime: DateTime.now(), + error: error, + ); + } +} + +/// YARA Rule Match +class YARAMatch { + final String ruleName; + final String description; + final String? author; + final String? reference; + final List tags; + final int threatLevel; + + const YARAMatch({ + required this.ruleName, + required this.description, + this.author, + this.reference, + this.tags = const [], + this.threatLevel = 1, + }); + + Map toJson() { + return { + 'ruleName': ruleName, + 'description': description, + 'author': author, + 'reference': reference, + 'tags': tags, + 'threatLevel': threatLevel, + }; + } +} + +/// YARA Rule +class YARARule { + final String name; + final String content; + final String? author; + final String? description; + final List tags; + + const YARARule({ + required this.name, + required this.content, + this.author, + this.description, + this.tags = const [], + }); + + factory YARARule.fromString(String ruleContent) { + final lines = ruleContent.split('\n'); + String? ruleName; + String? author; + String? description; + final tags = []; + + for (final line in lines) { + final trimmedLine = line.trim(); + if (trimmedLine.startsWith('rule ')) { + ruleName = trimmedLine.substring(5).trim().split(' ').first; + } else if (trimmedLine.startsWith('author = ')) { + author = trimmedLine.substring(9).trim().replaceAll('"', ''); + } else if (trimmedLine.startsWith('description = ')) { + description = trimmedLine.substring(13).trim().replaceAll('"', ''); + } else if (trimmedLine.startsWith('tags = ')) { + final tagString = trimmedLine.substring(7).trim().replaceAll('"', ''); + tags.addAll(tagString.split(',').map((t) => t.trim())); + } + } + + return YARARule( + name: ruleName ?? 'unknown', + content: ruleContent, + author: author, + description: description, + tags: tags, + ); + } +} + +/// Main YARA Scanner Class +class YARAScanner { + final YARAConfig config; + final List _rules = []; + Timer? _updateTimer; + + YARAScanner(this.config); + + /// Initialize the scanner + Future initialize() async { + await _loadRules(); + if (config.enableAutoUpdate) { + _startAutoUpdate(); + } + } + + /// Load YARA rules from local directory + Future _loadRules() async { + try { + final rulesDir = Directory(config.rulesDirectory); + if (!await rulesDir.exists()) { + await rulesDir.create(recursive: true); + } + + _rules.clear(); + + await for (final entity in rulesDir.list()) { + if (entity is File && entity.path.endsWith('.yar')) { + try { + final content = await entity.readAsString(); + final rule = YARARule.fromString(content); + _rules.add(rule); + } catch (e) { + print('Error loading rule ${entity.path}: $e'); + } + } + } + + print('Loaded ${_rules.length} YARA rules'); + } catch (e) { + print('Error loading YARA rules: $e'); + } + } + + /// Update rules from remote sources + Future updateRules() async { + try { + for (final source in config.ruleSources) { + try { + final response = await http.get(Uri.parse(source)); + if (response.statusCode == 200) { + final fileName = source.split('/').last; + final localPath = path.join(config.rulesDirectory, fileName); + + final file = File(localPath); + await file.writeAsString(response.body); + print('Updated rule: $fileName'); + } + } catch (e) { + print('Error updating rule from $source: $e'); + } + } + + await _loadRules(); + } catch (e) { + print('Error updating YARA rules: $e'); + } + } + + /// Start automatic rule updates + void _startAutoUpdate() { + _updateTimer?.cancel(); + _updateTimer = Timer.periodic(config.updateInterval, (_) { + updateRules(); + }); + } + + /// Scan a file for malware using YARA rules + Future scanFile(String filePath) async { + try { + final file = File(filePath); + if (!await file.exists()) { + return YARAScanResult.error(filePath, 'File not found'); + } + + final fileBytes = await file.readAsBytes(); + final fileContent = utf8.decode(fileBytes); + final matches = []; + + for (final rule in _rules) { + final match = await _checkRule(rule, fileContent, fileBytes); + if (match != null) { + matches.add(match); + } + } + + return YARAScanResult( + isInfected: matches.isNotEmpty, + matches: matches, + filePath: filePath, + scanTime: DateTime.now(), + ); + } catch (e) { + return YARAScanResult.error(filePath, 'Scan failed: $e'); + } + } + + /// Check if a file matches a specific YARA rule + Future _checkRule( + YARARule rule, + String fileContent, + List fileBytes, + ) async { + // Simple string matching (basic implementation) + // In a real implementation, you'd want to use proper YARA parsing + final ruleLines = rule.content.split('\n'); + final strings = []; + + for (final line in ruleLines) { + final trimmedLine = line.trim(); + if (trimmedLine.startsWith('condition:')) { + // condition = trimmedLine.substring(10).trim(); // Not used in basic implementation + } else if (trimmedLine.contains('\$') && trimmedLine.contains(' = ')) { + final stringMatch = RegExp(r'\$(\w+)\s*=\s*{([^}]+)}').firstMatch(trimmedLine); + if (stringMatch != null && stringMatch.group(1) != null) { + strings.add(stringMatch.group(1)!); + } + } + } + + // Check if any strings match + for (final string in strings) { + final stringPattern = RegExp(r'\$' + string + r'\s*=\s*{([^}]+)}'); + final stringMatch = stringPattern.firstMatch(rule.content); + if (stringMatch != null) { + final searchString = stringMatch.group(1)!.trim().replaceAll('"', ''); + if (fileContent.contains(searchString)) { + return YARAMatch( + ruleName: rule.name, + description: rule.description ?? 'No description available', + author: rule.author, + tags: rule.tags, + threatLevel: _calculateThreatLevel(rule.tags), + ); + } + } + } + + return null; + } + + /// Calculate threat level based on rule tags + int _calculateThreatLevel(List tags) { + if (tags.any((tag) => tag.toLowerCase().contains('trojan'))) return 5; + if (tags.any((tag) => tag.toLowerCase().contains('malware'))) return 4; + if (tags.any((tag) => tag.toLowerCase().contains('spyware'))) return 3; + if (tags.any((tag) => tag.toLowerCase().contains('adware'))) return 2; + return 1; + } + + /// Get file hash for additional verification + Future> getFileHashes(String filePath) async { + try { + final file = File(filePath); + final bytes = await file.readAsBytes(); + + final md5Hash = md5.convert(bytes); + final sha1Hash = sha1.convert(bytes); + final sha256Hash = sha256.convert(bytes); + + return { + 'md5': md5Hash.toString(), + 'sha1': sha1Hash.toString(), + 'sha256': sha256Hash.toString(), + }; + } catch (e) { + return { + 'error': 'Failed to calculate hashes: $e', + }; + } + } + + /// Dispose of the scanner + void dispose() { + _updateTimer?.cancel(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index ef6c14881..b8a573872 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,8 +46,10 @@ dependencies: html: ^0.15.6 shared_preferences: ^2.5.4 url_launcher: ^6.3.2 + path: ^1.9.0 permission_handler: ^12.0.1 fluttertoast: ^9.0.0 + crypto: ^3.0.6 device_info_plus: ^12.3.0 file_picker: ^10.3.10 animations: ^2.1.1 From 0be2e51192a7f30932a2f844af7713072658d7d6 Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 22 Feb 2026 02:54:56 +0200 Subject: [PATCH 02/46] Re-add Settings page --- lib/pages/home.dart | 4 +- lib/pages/security_settings.dart | 344 ------------------------------- lib/pages/settings.dart | 61 ++++++ 3 files changed, 63 insertions(+), 346 deletions(-) delete mode 100644 lib/pages/security_settings.dart diff --git a/lib/pages/home.dart b/lib/pages/home.dart index ae409b853..255a0020d 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -11,7 +11,7 @@ import 'package:updatium/custom_errors.dart'; import 'package:updatium/pages/add_app.dart'; import 'package:updatium/pages/apps.dart'; import 'package:updatium/pages/security_disclaimer.dart'; -import 'package:updatium/pages/security_settings.dart'; +import 'package:updatium/pages/settings.dart'; import 'package:updatium/providers/apps_provider.dart'; import 'package:updatium/providers/settings_provider.dart'; import 'package:updatium/providers/source_provider.dart'; @@ -55,7 +55,7 @@ class _HomePageState extends State with TickerProviderStateMixin { Icons.add_circle, AddAppPage(key: GlobalKey()), ), - NavigationPageItem(tr('settings'), Icons.settings, const SecuritySettingsPage()), + NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()), ]; @override diff --git a/lib/pages/security_settings.dart b/lib/pages/security_settings.dart deleted file mode 100644 index 2d7cdc3c8..000000000 --- a/lib/pages/security_settings.dart +++ /dev/null @@ -1,344 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:updatium/security/security_settings_provider.dart'; - -class SecuritySettingsPage extends StatefulWidget { - const SecuritySettingsPage({super.key}); - - @override - State createState() => _SecuritySettingsPageState(); -} - -class _SecuritySettingsPageState extends State { - late SecuritySettingsProvider _securityProvider; - bool _isLoading = false; - - @override - void initState() { - super.initState(); - _initializeSecurityProvider(); - } - - Future _initializeSecurityProvider() async { - _securityProvider = await SecuritySettingsProvider.create(); - await _securityProvider.initialize(); - setState(() {}); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(tr('securitySettings')), - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - ), - body: ListView( - padding: const EdgeInsets.all(16.0), - children: [ - _buildHeader(), - const SizedBox(height: 16), - _buildAutoScanSection(), - const SizedBox(height: 16), - _buildAutoUpdateSection(), - const SizedBox(height: 16), - _buildThreatLevelSection(), - const SizedBox(height: 16), - _buildQuarantineSection(), - const SizedBox(height: 16), - _buildUpdateSection(), - ], - ), - ); - } - - Widget _buildHeader() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.security, - color: Theme.of(context).colorScheme.primary, - size: 24, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr('yaraMalwareScanner'), - style: Theme.of(context).textTheme.titleLarge, - ), - Text( - tr('yaraScannerDescription'), - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildAutoScanSection() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr('autoScanSettings'), - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - SwitchListTile( - title: Text(tr('enableAutoScan')), - subtitle: Text(tr('enableAutoScanDescription')), - value: _securityProvider.getAutoScanEnabled(), - onChanged: (value) async { - await _securityProvider.setAutoScanEnabled(value); - setState(() {}); - }, - ), - ], - ), - ), - ); - } - - Widget _buildAutoUpdateSection() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr('autoUpdateSettings'), - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - SwitchListTile( - title: Text(tr('enableAutoUpdate')), - subtitle: Text(tr('enableAutoUpdateDescription')), - value: _securityProvider.getAutoUpdateEnabled(), - onChanged: (value) async { - await _securityProvider.setAutoUpdateEnabled(value); - setState(() {}); - }, - ), - const SizedBox(height: 8), - ListTile( - title: Text(tr('updateInterval')), - subtitle: Text(tr('updateIntervalDescription')), - trailing: DropdownButton( - value: _securityProvider.getUpdateInterval(), - items: [1, 6, 12, 24, 48, 72].map((hours) { - return DropdownMenuItem( - value: hours, - child: Text('$hours ${tr('hours')}'), - ); - }).toList(), - onChanged: (value) async { - if (value != null) { - await _securityProvider.setUpdateInterval(value); - setState(() {}); - } - }, - ), - ), - ], - ), - ), - ); - } - - Widget _buildThreatLevelSection() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr('threatLevelFilter'), - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Text( - tr('threatLevelFilterDescription'), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 8), - RadioListTile( - title: Text(tr('level1')), - subtitle: Text(tr('lowThreat')), - value: 1, - groupValue: _securityProvider.getThreatLevelFilter(), - onChanged: (value) async { - if (value != null) { - await _securityProvider.setThreatLevelFilter(value); - setState(() {}); - } - }, - ), - RadioListTile( - title: Text(tr('level2')), - subtitle: Text(tr('mediumThreat')), - value: 2, - groupValue: _securityProvider.getThreatLevelFilter(), - onChanged: (value) async { - if (value != null) { - await _securityProvider.setThreatLevelFilter(value); - setState(() {}); - } - }, - ), - RadioListTile( - title: Text(tr('level3')), - subtitle: Text(tr('highThreat')), - value: 3, - groupValue: _securityProvider.getThreatLevelFilter(), - onChanged: (value) async { - if (value != null) { - await _securityProvider.setThreatLevelFilter(value); - setState(() {}); - } - }, - ), - ], - ), - ), - ); - } - - Widget _buildQuarantineSection() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr('quarantineSettings'), - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - SwitchListTile( - title: Text(tr('quarantineInfected')), - subtitle: Text(tr('quarantineInfectedDescription')), - value: _securityProvider.getQuarantineInfected(), - onChanged: (value) async { - await _securityProvider.setQuarantineInfected(value); - setState(() {}); - }, - ), - const SizedBox(height: 8), - ListTile( - title: Text(tr('viewQuarantine')), - subtitle: Text(tr('viewQuarantineDescription')), - trailing: const Icon(Icons.folder), - onTap: () { - // TODO: Navigate to quarantine view - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(tr('quarantineViewComingSoon'))), - ); - }, - ), - ], - ), - ), - ); - } - - Widget _buildUpdateSection() { - final lastUpdate = _securityProvider.getLastUpdate(); - final rulesVersion = _securityProvider.getRulesVersion(); - - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr('databaseInformation'), - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - ListTile( - title: Text(tr('lastUpdate')), - subtitle: lastUpdate != null - ? Text(lastUpdate.toString()) - : Text(tr('never')), - trailing: const Icon(Icons.update), - ), - ListTile( - title: Text(tr('rulesVersion')), - subtitle: Text(rulesVersion ?? tr('unknown')), - trailing: const Icon(Icons.code), - ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _isLoading ? null : _updateRules, - icon: _isLoading - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.download), - label: Text(_isLoading ? tr('updating') : tr('updateNow')), - ), - ), - ], - ), - ), - ); - } - - Future _updateRules() async { - setState(() { - _isLoading = true; - }); - - try { - await _securityProvider.updateRules(); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(tr('rulesUpdatedSuccessfully'))), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(tr('rulesUpdateFailed'))), - ); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - void dispose() { - _securityProvider.dispose(); - super.dispose(); - } -} diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index c42447389..a6616ab22 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -17,6 +17,7 @@ import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:shizuku_apk_installer/shizuku_apk_installer.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:updatium/security/security_settings_provider.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -26,6 +27,7 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { + late SecuritySettingsProvider _securityProvider; List updateIntervalNodes = [ 15, 30, @@ -95,6 +97,11 @@ class _SettingsPageState extends State { } } + Future _initializeSecurityProvider() async { + _securityProvider = await SecuritySettingsProvider.create(); + await _securityProvider.initialize(); + } + @override Widget build(BuildContext context) { SettingsProvider settingsProvider = context.watch(); @@ -103,6 +110,9 @@ class _SettingsPageState extends State { initUpdateIntervalInterpolator(); processIntervalSliderValue(settingsProvider.updateIntervalSliderVal); + // Initialize security provider + _initializeSecurityProvider(); + var followSystemThemeExplanation = FutureBuilder( builder: (ctx, val) { return ((val.data?.version.sdkInt ?? 30) < 29) @@ -752,6 +762,57 @@ class _SettingsPageState extends State { ), ...sourceSpecificFields, height32, + Text( + tr('yaraMalwareScanner'), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + height16, + Text( + tr('yaraScannerDescription'), + style: Theme.of(context).textTheme.bodyMedium, + ), + height16, + SwitchListTile( + title: Text(tr('enableAutoScan')), + subtitle: Text(tr('enableAutoScanDescription')), + value: _securityProvider.getAutoScanEnabled(), + onChanged: (value) async { + await _securityProvider.setAutoScanEnabled(value); + setState(() {}); + }, + ), + SwitchListTile( + title: Text(tr('enableAutoUpdate')), + subtitle: Text(tr('enableAutoUpdateDescription')), + value: _securityProvider.getAutoUpdateEnabled(), + onChanged: (value) async { + await _securityProvider.setAutoUpdateEnabled(value); + setState(() {}); + }, + ), + ListTile( + title: Text(tr('updateInterval')), + subtitle: Text(tr('updateIntervalDescription')), + trailing: DropdownButton( + value: _securityProvider.getUpdateInterval(), + items: [1, 6, 12, 24, 48, 72].map((hours) { + return DropdownMenuItem( + value: hours, + child: Text('$hours ${tr('hours')}'), + ); + }).toList(), + onChanged: (value) async { + if (value != null) { + await _securityProvider.setUpdateInterval(value); + setState(() {}); + } + }, + ), + ), + height32, Text( tr('appearance'), style: TextStyle( From 0db7b30436eedeb802753d8982999403b7944157 Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 22 Feb 2026 03:12:58 +0200 Subject: [PATCH 03/46] Fix --- assets/translations/de.json | 10 +++ assets/translations/en.json | 4 +- assets/translations/fr.json | 11 ++++ lib/pages/settings.dart | 28 +++++++++ lib/providers/apps_provider.dart | 34 +++++++++-- lib/security/security_settings_provider.dart | 11 ++-- lib/security/yara_scanner.dart | 64 +++++++++++++++++--- 7 files changed, 143 insertions(+), 19 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index d5a18407c..7aa81b9ef 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -28,9 +28,19 @@ "githubStarredRepos": "GitHub Starred Repos", "uname": "Benutzername", "wrongArgNum": "Falsche Anzahl von Argumenten (Parametern) übermittelt", + "yaraMalwareScanner": "Automatische Malware-Erkennung (Powered by YARA)", + "yaraScannerDescription": "Branchenstandardmäßige Malware-Erkennung mit YARA-kompatiblen Regeln für umfassenden Schutz vor Viren, Trojanern und anderen Bedrohungen. Kein Scanner kann 100% Erkennung garantieren, bietet aber starken Schutz vor bekannter Malware. Die Verantwortung für die sichere App-Nutzung liegt letztlich beim Benutzer.", + "securitySettings": "Sicherheitseinstellungen", "xIsTrackOnly": "{} ist nur zur Nachverfolgung", "source": "Quelle", "app": "App", + "enableAutoScan": "Automatische Scan aktivieren", + "enableAutoScanDescription": "Heruntergeladene APKs automatisch auf Malware scannen, vor der Installation", + "enableAutoUpdate": "Automatische Updates aktivieren", + "enableAutoUpdateDescription": "Malware-Definitions-Datenbank automatisch aktualisieren", + "updateInterval": "Update-Intervall", + "updateIntervalDescription": "Wie oft nach neuen Malware-Definitionen gesucht werden soll", + "hours": "Stunden", "appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind nur zur Nachverfolgung.", "youPickedTrackOnly": "Sie haben die Option „Nur nachverfolgen“ gewählt.", "trackOnlyAppDescription": "Die App wird auf neue verfügbare Versionen überwacht, aber Updatium wird sie nicht herunterladen oder installieren.", diff --git a/assets/translations/en.json b/assets/translations/en.json index be2090e78..ac31c8cd4 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -28,8 +28,8 @@ "githubStarredRepos": "GitHub starred repositories", "uname": "Username", "wrongArgNum": "Wrong number of arguments provided", - "yaraMalwareScanner": "YARA Malware Scanner", - "yaraScannerDescription": "Industry-standard malware detection using YARA rules for comprehensive protection against viruses, trojans, and other threats.", + "yaraMalwareScanner": "Auto Malware Scanning (Powered by YARA)", + "yaraScannerDescription": "Industry-standard malware detection using YARA rules for comprehensive protection against viruses, trojans, and other threats. While no scanner can guarantee 100% detection, this provides strong protection against known malware. The responsibility for safe app usage ultimately is on the user.", "securitySettings": "Security Settings", "enableAutoScan": "Enable Auto-Scan", "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 9666d7228..46576050c 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -28,12 +28,23 @@ "githubStarredRepos": "dépôts étoilés GitHub", "uname": "Nom d'utilisateur", "wrongArgNum": "Nombre incorrect des arguments fournis", + "yaraMalwareScanner": "Analyse Antimalware Automatique (Alimenté par YARA)", + "yaraScannerDescription": "Détection de logiciels malveillants de qualité industrielle utilisant des règles compatibles YARA pour une protection complète contre les virus, chevaux de Troie et autres menaces. Bien qu'aucun scanner ne puisse garantir 100% de détection, cela offre une protection solide contre les logiciels malveillants connus. La responsabilité de l'utilisation sécurisée des applications incombe finalement à l'utilisateur.", + "securitySettings": "Paramètres de sécurité", "xIsTrackOnly": "{} en Suivi uniquement", "source": "source", "app": "Appli", + "enableAutoScan": "Activer l'analyse automatique", + "enableAutoScanDescription": "Analyser automatiquement les APK téléchargées à la recherche de logiciels malveillants avant installation", + "enableAutoUpdate": "Activer les mises à jour automatiques", + "enableAutoUpdateDescription": "Mettre à jour automatiquement la base de données des définitions de logiciels malveillants", + "updateInterval": "Intervalle de mise à jour", + "updateIntervalDescription": "Fréquence de recherche des nouvelles définitions de logiciels malveillants", + "hours": "heures", "appsFromSourceAreTrackOnly": "Les applications de cette source sont en 'Suivi uniquement'.", "youPickedTrackOnly": "Vous avez sélectionné l'option 'Suivi uniquement'.", "trackOnlyAppDescription": "L'application sera suivie pour les mises à jour, mais Updatium ne pourra pas la télécharger ou l'installer.", + "trackOnlyAppDescription": "L'application sera suivie pour les mises à jour, mais Updatium ne pourra pas la télécharger ou l'installer.", "cancelled": "Annulé", "appAlreadyAdded": "Application déjà ajoutée", "alreadyUpToDateQuestion": "L'application est déjà à jour?", diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index a6616ab22..5359ea339 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -812,6 +812,34 @@ class _SettingsPageState extends State { }, ), ), + height16, + // Update button with error handling + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () async { + try { + await securityProvider.updateRules(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(tr('rulesUpdatedSuccessfully'))), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tr('rulesUpdateFailed')), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } + }, + icon: const Icon(Icons.download), + label: Text(tr('updateNow')), + ), + ), height32, Text( tr('appearance'), diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 204e2b293..293c17471 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -947,23 +947,47 @@ class AppsProvider with ChangeNotifier { } /// Scan APK for malware before installation - Future _scanAPKForMalware(String apkPath) async { + Future _scanAPKForMalware(String apkPath, {List? additionalApkPaths}) async { try { final securityProvider = await SecuritySettingsProvider.create(); await securityProvider.initialize(); + + // Scan base APK final scanResult = await securityProvider.scanAPK(apkPath); + if (scanResult.error != null) { + logs.add('Security scan error: ${scanResult.error}'); + // On scan error, be conservative and block installation + return false; + } + if (scanResult.isInfected) { logs.add('Security scan detected malware in APK: ${scanResult.matches.map((m) => m.ruleName).join(', ')}'); return false; // Block installation } + // Scan additional APKs if present (split APKs) + if (additionalApkPaths != null) { + for (final additionalApkPath in additionalApkPaths) { + final additionalScanResult = await securityProvider.scanAPK(additionalApkPath); + + if (additionalScanResult.error != null) { + logs.add('Security scan error for additional APK: ${additionalScanResult.error}'); + return false; // Block installation on error + } + + if (additionalScanResult.isInfected) { + logs.add('Security scan detected malware in additional APK: ${additionalScanResult.matches.map((m) => m.ruleName).join(', ')}'); + return false; // Block installation + } + } + } + return true; // Safe to install } catch (e) { logs.add('Security scan failed: $e'); - return true; // Allow installation on scan failure - } finally { - // Security provider will be disposed by caller if needed + // On scan failure, be conservative and block installation + return false; // CRITICAL: Always block on exception } } @@ -1005,7 +1029,7 @@ class AppsProvider with ChangeNotifier { PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); // Security scan before installation - if (!(await _scanAPKForMalware(file.file.path))) { + if (!(await _scanAPKForMalware(file.file.path, additionalApkPaths: additionalAPKs.map((a) => a.file.path).toList()))) { throw UpdatiumError('Security scan detected malware. Installation blocked for safety.'); } diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart index 086b7514c..3c0b2b530 100644 --- a/lib/security/security_settings_provider.dart +++ b/lib/security/security_settings_provider.dart @@ -19,18 +19,18 @@ class SecuritySettingsProvider { late YARAConfig _config; late YARAScanner _scanner; - SecuritySettingsProvider._(this._prefs) { + SecuritySettingsProvider(this._prefs) { _config = YARAConfig( rulesDirectory: _getRulesDirectory(), updateInterval: Duration(hours: getUpdateInterval()), enableAutoUpdate: getAutoUpdateEnabled(), ); - _scanner = YARAScanner(_config); + _scanner = YARAScanner.getInstance(_config); } static Future create() async { final prefs = await SharedPreferences.getInstance(); - return SecuritySettingsProvider._(prefs); + return SecuritySettingsProvider(prefs); } /// Get the directory for YARA rules @@ -132,7 +132,8 @@ class SecuritySettingsProvider { await setLastUpdate(DateTime.now()); await setRulesVersion('latest-${DateTime.now().millisecondsSinceEpoch}'); } catch (e) { - print('Error updating rules: $e'); + // Re-throw the exception so callers can handle it + throw Exception('Failed to update YARA rules: $e'); } } @@ -141,6 +142,6 @@ class SecuritySettingsProvider { /// Dispose resources void dispose() { - _scanner.dispose(); + // Don't dispose singleton here, let it be managed globally } } diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index 295634a4f..2415f23a8 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -1,6 +1,6 @@ +import 'dart:async'; import 'dart:io'; import 'dart:convert'; -import 'dart:async'; import 'package:path/path.dart' as path; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; @@ -133,8 +133,16 @@ class YARAScanner { final YARAConfig config; final List _rules = []; Timer? _updateTimer; + static YARAScanner? _instance; + + /// Get singleton instance + static YARAScanner getInstance(YARAConfig config) { + _instance ??= YARAScanner._(config); + return _instance!; + } - YARAScanner(this.config); + /// Private constructor for singleton + YARAScanner._(this.config); /// Initialize the scanner Future initialize() async { @@ -214,11 +222,10 @@ class YARAScanner { } final fileBytes = await file.readAsBytes(); - final fileContent = utf8.decode(fileBytes); final matches = []; for (final rule in _rules) { - final match = await _checkRule(rule, fileContent, fileBytes); + final match = await _checkRule(rule, fileBytes); if (match != null) { matches.add(match); } @@ -238,7 +245,6 @@ class YARAScanner { /// Check if a file matches a specific YARA rule Future _checkRule( YARARule rule, - String fileContent, List fileBytes, ) async { // Simple string matching (basic implementation) @@ -258,13 +264,33 @@ class YARAScanner { } } - // Check if any strings match + // Check if any strings match in binary data for (final string in strings) { final stringPattern = RegExp(r'\$' + string + r'\s*=\s*{([^}]+)}'); final stringMatch = stringPattern.firstMatch(rule.content); if (stringMatch != null) { final searchString = stringMatch.group(1)!.trim().replaceAll('"', ''); - if (fileContent.contains(searchString)) { + + // Convert search string to bytes for binary comparison + List searchBytes; + try { + searchBytes = utf8.encode(searchString); + } catch (e) { + // Handle hex strings like {6A 40 68 00 30 00 00} + final hexString = searchString.replaceAll(RegExp(r'[{} ]'), ''); + searchBytes = []; + for (int i = 0; i < hexString.length; i += 2) { + if (i + 1 < hexString.length) { + final byte = int.tryParse(hexString.substring(i, i + 2), radix: 16); + if (byte != null) { + searchBytes.add(byte); + } + } + } + } + + // Search for bytes in the file + if (_containsBytes(fileBytes, searchBytes)) { return YARAMatch( ruleName: rule.name, description: rule.description ?? 'No description available', @@ -279,6 +305,24 @@ class YARAScanner { return null; } + /// Helper method to check if byte sequence contains another byte sequence + bool _containsBytes(List data, List pattern) { + if (pattern.isEmpty) return true; + if (data.length < pattern.length) return false; + + for (int i = 0; i <= data.length - pattern.length; i++) { + bool match = true; + for (int j = 0; j < pattern.length; j++) { + if (data[i + j] != pattern[j]) { + match = false; + break; + } + } + if (match) return true; + } + return false; + } + /// Calculate threat level based on rule tags int _calculateThreatLevel(List tags) { if (tags.any((tag) => tag.toLowerCase().contains('trojan'))) return 5; @@ -314,4 +358,10 @@ class YARAScanner { void dispose() { _updateTimer?.cancel(); } + + /// Global dispose method to cleanup singleton + static void disposeGlobal() { + _instance?.dispose(); + _instance = null; + } } From e53374ea96feeaef76eeeca593eab3f177ef5403 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:14:32 +0200 Subject: [PATCH 04/46] Update lib/security/yara_scanner.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/security/yara_scanner.dart | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index 2415f23a8..1f71fdf5a 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -213,13 +213,12 @@ class YARAScanner { }); } - /// Scan a file for malware using YARA rules - Future scanFile(String filePath) async { - try { - final file = File(filePath); - if (!await file.exists()) { - return YARAScanResult.error(filePath, 'File not found'); - } + final fileBytes = await file.readAsBytes(); + final matches = []; + + for (final rule in _rules) { + final match = await _checkRule(rule, fileBytes); + if (match != null) { final fileBytes = await file.readAsBytes(); final matches = []; From 54db2559886c6673f36c5afa8c681f0f42e3c074 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:15:07 +0200 Subject: [PATCH 05/46] Update lib/providers/apps_provider.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/providers/apps_provider.dart | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 293c17471..6f149359b 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -947,24 +947,27 @@ class AppsProvider with ChangeNotifier { } /// Scan APK for malware before installation - Future _scanAPKForMalware(String apkPath, {List? additionalApkPaths}) async { + Future _scanAPKForMalware(String apkPath) async { + SecuritySettingsProvider? securityProvider; try { - final securityProvider = await SecuritySettingsProvider.create(); + securityProvider = await SecuritySettingsProvider.create(); await securityProvider.initialize(); - - // Scan base APK final scanResult = await securityProvider.scanAPK(apkPath); - - if (scanResult.error != null) { - logs.add('Security scan error: ${scanResult.error}'); - // On scan error, be conservative and block installation - return false; - } - + if (scanResult.isInfected) { - logs.add('Security scan detected malware in APK: ${scanResult.matches.map((m) => m.ruleName).join(', ')}'); + logs.add( + 'Security scan detected malware in APK: ${scanResult.matches.map((m) => m.ruleName).join(', ')}', + ); return false; // Block installation } + + return true; // Safe to install + } catch (e) { + logs.add('Security scan failed: $e'); + return true; // Allow installation on scan failure + } finally { + securityProvider?.dispose(); + } // Scan additional APKs if present (split APKs) if (additionalApkPaths != null) { From d9696021fb6b6e490ea179f975951f30d46b7926 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:15:37 +0200 Subject: [PATCH 06/46] Update lib/security/security_settings_provider.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/security/security_settings_provider.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart index 3c0b2b530..ace657ac7 100644 --- a/lib/security/security_settings_provider.dart +++ b/lib/security/security_settings_provider.dart @@ -105,10 +105,14 @@ class SecuritySettingsProvider { final fileName = path.basename(filePath); final timestamp = DateTime.now().millisecondsSinceEpoch; final quarantinedPath = '${quarantineDir.path}/$timestamp-$fileName'; - - await originalFile.rename(quarantinedPath); - - // Save scan report + + try { + await originalFile.rename(quarantinedPath); + } catch (_) { + await originalFile.copy(quarantinedPath); + await originalFile.delete(); + } + final reportPath = '${quarantineDir.path}/$timestamp-$fileName-report.json'; final report = { 'originalPath': filePath, @@ -116,9 +120,8 @@ class SecuritySettingsProvider { 'scanTime': result.scanTime.toIso8601String(), 'matches': result.matches.map((m) => m.toJson()).toList(), }; - + await File(reportPath).writeAsString(jsonEncode(report)); - print('File quarantined: $quarantinedPath'); } catch (e) { print('Error quarantining file: $e'); From d5e0e28a4e79b3e00d07415f0dae9064cb8de63e Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:19:18 +0200 Subject: [PATCH 07/46] Update lib/security/yara_scanner.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/security/yara_scanner.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index 1f71fdf5a..44207169d 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -213,13 +213,10 @@ class YARAScanner { }); } - final fileBytes = await file.readAsBytes(); - final matches = []; - - for (final rule in _rules) { - final match = await _checkRule(rule, fileBytes); - if (match != null) { - + /// Scan a file for malware + Future scanFile(String filePath) async { + try { + final file = File(filePath); final fileBytes = await file.readAsBytes(); final matches = []; From a58a3142689715897860f9890812f2c7b71ecfbe Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:20:28 +0200 Subject: [PATCH 08/46] Update lib/providers/apps_provider.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/providers/apps_provider.dart | 58 +++++++++++++++++++------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 6f149359b..c9f06e327 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -965,34 +965,44 @@ class AppsProvider with ChangeNotifier { } catch (e) { logs.add('Security scan failed: $e'); return true; // Allow installation on scan failure - } finally { - securityProvider?.dispose(); - } - - // Scan additional APKs if present (split APKs) - if (additionalApkPaths != null) { - for (final additionalApkPath in additionalApkPaths) { - final additionalScanResult = await securityProvider.scanAPK(additionalApkPath); - - if (additionalScanResult.error != null) { - logs.add('Security scan error for additional APK: ${additionalScanResult.error}'); - return false; // Block installation on error - } - - if (additionalScanResult.isInfected) { - logs.add('Security scan detected malware in additional APK: ${additionalScanResult.matches.map((m) => m.ruleName).join(', ')}'); - return false; // Block installation + Future _scanAPKForMalware(String apkPath, {List? additionalApkPaths}) async { + SecuritySettingsProvider? securityProvider; + try { + securityProvider = await SecuritySettingsProvider.create(); + await securityProvider.initialize(); + final scanResult = await securityProvider.scanAPK(apkPath); + + if (scanResult.isInfected) { + logs.add( + 'Security scan detected malware in APK: ${scanResult.matches.map((m) => m.ruleName).join(', ')}', + ); + return false; + } + + if (additionalApkPaths != null) { + for (final additionalApkPath in additionalApkPaths) { + final additionalScanResult = await securityProvider.scanAPK(additionalApkPath); + + if (additionalScanResult.error != null) { + logs.add('Security scan error for additional APK: ${additionalScanResult.error}'); + return false; + } + + if (additionalScanResult.isInfected) { + logs.add('Security scan detected malware in additional APK: ${additionalScanResult.matches.map((m) => m.ruleName).join(', ')}'); + return false; + } } } + + return true; + } catch (e) { + logs.add('Security scan failed: $e'); + return true; + } finally { + securityProvider?.dispose(); } - - return true; // Safe to install - } catch (e) { - logs.add('Security scan failed: $e'); - // On scan failure, be conservative and block installation - return false; // CRITICAL: Always block on exception } - } Future installApk( DownloadedApk file, From 1fdde1e7f578e0f6269e36a50709fd8c3dfe23e0 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:21:03 +0200 Subject: [PATCH 09/46] Update lib/pages/settings.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/pages/settings.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 5359ea339..02727f70e 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -819,7 +819,7 @@ class _SettingsPageState extends State { child: ElevatedButton.icon( onPressed: () async { try { - await securityProvider.updateRules(); + await _securityProvider.updateRules(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('rulesUpdatedSuccessfully'))), From f7a6cbbd3b5d5d2d89a3e72733c90114d291471e Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:21:43 +0200 Subject: [PATCH 10/46] Update lib/security/security_settings_provider.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/security/security_settings_provider.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart index ace657ac7..a1f289e3a 100644 --- a/lib/security/security_settings_provider.dart +++ b/lib/security/security_settings_provider.dart @@ -34,8 +34,8 @@ class SecuritySettingsProvider { } /// Get the directory for YARA rules - String _getRulesDirectory() { - final appDir = Directory.systemTemp.parent; + Future _getRulesDirectory() async { + final appDir = await getApplicationSupportDirectory(); final rulesDir = Directory('${appDir.path}/yara_rules'); return rulesDir.path; } From bf22d77c5fd2db1b9456376935056a703a5d54f8 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:25:23 +0200 Subject: [PATCH 11/46] Update lib/pages/settings.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/pages/settings.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 02727f70e..3b28aabca 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -110,8 +110,7 @@ class _SettingsPageState extends State { initUpdateIntervalInterpolator(); processIntervalSliderValue(settingsProvider.updateIntervalSliderVal); - // Initialize security provider - _initializeSecurityProvider(); + // Initialization is handled in initState, not build var followSystemThemeExplanation = FutureBuilder( builder: (ctx, val) { From 142e02a52ff7cf1287d4680b041e18e76ccdf428 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:26:46 +0200 Subject: [PATCH 12/46] Update lib/security/security_settings_provider.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/security/security_settings_provider.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart index a1f289e3a..bbc01c200 100644 --- a/lib/security/security_settings_provider.dart +++ b/lib/security/security_settings_provider.dart @@ -19,15 +19,22 @@ class SecuritySettingsProvider { late YARAConfig _config; late YARAScanner _scanner; - SecuritySettingsProvider(this._prefs) { + SecuritySettingsProvider(this._prefs, String rulesDirectory) { _config = YARAConfig( - rulesDirectory: _getRulesDirectory(), + rulesDirectory: rulesDirectory, updateInterval: Duration(hours: getUpdateInterval()), enableAutoUpdate: getAutoUpdateEnabled(), ); _scanner = YARAScanner.getInstance(_config); } + static Future create() async { + final prefs = await SharedPreferences.getInstance(); + final appDir = await getApplicationSupportDirectory(); + final rulesDir = '${appDir.path}/yara_rules'; + return SecuritySettingsProvider(prefs, rulesDir); + } + static Future create() async { final prefs = await SharedPreferences.getInstance(); return SecuritySettingsProvider(prefs); From bd06ccc8062e63b94b17d2db6c011dd6722bbcf5 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:27:18 +0200 Subject: [PATCH 13/46] Update lib/security/security_settings_provider.dart Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com> --- lib/security/security_settings_provider.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart index bbc01c200..dc6b8c87d 100644 --- a/lib/security/security_settings_provider.dart +++ b/lib/security/security_settings_provider.dart @@ -95,8 +95,9 @@ class SecuritySettingsProvider { // Handle quarantine if enabled if (result.isInfected && getQuarantineInfected()) { await _quarantineFile(apkPath, result); - } - + try { + final rulesDir = await _getRulesDirectory(); + final quarantineDir = Directory('$rulesDir/quarantine'); return result; } From 575928957479e8571ac684919f49aaa6288acf24 Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Mon, 23 Feb 2026 14:12:27 +0200 Subject: [PATCH 14/46] Another fix commit --- lib/pages/settings.dart | 32 +++-- lib/providers/apps_provider.dart | 58 +++------ lib/security/security_settings_provider.dart | 29 +++-- lib/security/yara_scanner.dart | 123 +++++++++++++++---- 4 files changed, 158 insertions(+), 84 deletions(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 3b28aabca..c95bdc611 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -28,6 +28,7 @@ class SettingsPage extends StatefulWidget { class _SettingsPageState extends State { late SecuritySettingsProvider _securityProvider; + bool _securityProviderInitialized = false; List updateIntervalNodes = [ 15, 30, @@ -100,6 +101,15 @@ class _SettingsPageState extends State { Future _initializeSecurityProvider() async { _securityProvider = await SecuritySettingsProvider.create(); await _securityProvider.initialize(); + setState(() { + _securityProviderInitialized = true; + }); + } + + @override + void initState() { + super.initState(); + _initializeSecurityProvider(); } @override @@ -777,38 +787,38 @@ class _SettingsPageState extends State { SwitchListTile( title: Text(tr('enableAutoScan')), subtitle: Text(tr('enableAutoScanDescription')), - value: _securityProvider.getAutoScanEnabled(), - onChanged: (value) async { + value: _securityProviderInitialized ? _securityProvider.getAutoScanEnabled() : false, + onChanged: _securityProviderInitialized ? (value) async { await _securityProvider.setAutoScanEnabled(value); setState(() {}); - }, + } : null, ), SwitchListTile( title: Text(tr('enableAutoUpdate')), subtitle: Text(tr('enableAutoUpdateDescription')), - value: _securityProvider.getAutoUpdateEnabled(), - onChanged: (value) async { + value: _securityProviderInitialized ? _securityProvider.getAutoUpdateEnabled() : false, + onChanged: _securityProviderInitialized ? (value) async { await _securityProvider.setAutoUpdateEnabled(value); setState(() {}); - }, + } : null, ), ListTile( title: Text(tr('updateInterval')), subtitle: Text(tr('updateIntervalDescription')), trailing: DropdownButton( - value: _securityProvider.getUpdateInterval(), + value: _securityProviderInitialized ? _securityProvider.getUpdateInterval() : 1, items: [1, 6, 12, 24, 48, 72].map((hours) { return DropdownMenuItem( value: hours, child: Text('$hours ${tr('hours')}'), ); }).toList(), - onChanged: (value) async { + onChanged: _securityProviderInitialized ? (value) async { if (value != null) { await _securityProvider.setUpdateInterval(value); setState(() {}); } - }, + } : null, ), ), height16, @@ -816,7 +826,7 @@ class _SettingsPageState extends State { SizedBox( width: double.infinity, child: ElevatedButton.icon( - onPressed: () async { + onPressed: _securityProviderInitialized ? () async { try { await _securityProvider.updateRules(); if (mounted) { @@ -834,7 +844,7 @@ class _SettingsPageState extends State { ); } } - }, + } : null, icon: const Icon(Icons.download), label: Text(tr('updateNow')), ), diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index c9f06e327..cd708260b 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -947,7 +947,7 @@ class AppsProvider with ChangeNotifier { } /// Scan APK for malware before installation - Future _scanAPKForMalware(String apkPath) async { + Future _scanAPKForMalware(String apkPath, {List? additionalApkPaths}) async { SecuritySettingsProvider? securityProvider; try { securityProvider = await SecuritySettingsProvider.create(); @@ -958,51 +958,33 @@ class AppsProvider with ChangeNotifier { logs.add( 'Security scan detected malware in APK: ${scanResult.matches.map((m) => m.ruleName).join(', ')}', ); - return false; // Block installation + return false; } - return true; // Safe to install - } catch (e) { - logs.add('Security scan failed: $e'); - return true; // Allow installation on scan failure - Future _scanAPKForMalware(String apkPath, {List? additionalApkPaths}) async { - SecuritySettingsProvider? securityProvider; - try { - securityProvider = await SecuritySettingsProvider.create(); - await securityProvider.initialize(); - final scanResult = await securityProvider.scanAPK(apkPath); + if (additionalApkPaths != null) { + for (final additionalApkPath in additionalApkPaths) { + final additionalScanResult = await securityProvider.scanAPK(additionalApkPath); - if (scanResult.isInfected) { - logs.add( - 'Security scan detected malware in APK: ${scanResult.matches.map((m) => m.ruleName).join(', ')}', - ); - return false; - } - - if (additionalApkPaths != null) { - for (final additionalApkPath in additionalApkPaths) { - final additionalScanResult = await securityProvider.scanAPK(additionalApkPath); - - if (additionalScanResult.error != null) { - logs.add('Security scan error for additional APK: ${additionalScanResult.error}'); - return false; - } + if (additionalScanResult.error != null) { + logs.add('Security scan error for additional APK: ${additionalScanResult.error}'); + return false; + } - if (additionalScanResult.isInfected) { - logs.add('Security scan detected malware in additional APK: ${additionalScanResult.matches.map((m) => m.ruleName).join(', ')}'); - return false; - } + if (additionalScanResult.isInfected) { + logs.add('Security scan detected malware in additional APK: ${additionalScanResult.matches.map((m) => m.ruleName).join(', ')}'); + return false; } } - - return true; - } catch (e) { - logs.add('Security scan failed: $e'); - return true; - } finally { - securityProvider?.dispose(); } + + return true; + } catch (e) { + logs.add('Security scan failed: $e'); + return true; // Allow installation on scan failure + } finally { + securityProvider?.dispose(); } + } Future installApk( DownloadedApk file, diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart index dc6b8c87d..22ecc266b 100644 --- a/lib/security/security_settings_provider.dart +++ b/lib/security/security_settings_provider.dart @@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:path/path.dart' as path; import 'dart:convert'; import 'package:updatium/security/yara_scanner.dart'; +import 'package:updatium/providers/logs_provider.dart'; /// Security Settings Provider class SecuritySettingsProvider { @@ -18,6 +19,7 @@ class SecuritySettingsProvider { final SharedPreferences _prefs; late YARAConfig _config; late YARAScanner _scanner; + final LogsProvider _logs = LogsProvider(); SecuritySettingsProvider(this._prefs, String rulesDirectory) { _config = YARAConfig( @@ -35,11 +37,6 @@ class SecuritySettingsProvider { return SecuritySettingsProvider(prefs, rulesDir); } - static Future create() async { - final prefs = await SharedPreferences.getInstance(); - return SecuritySettingsProvider(prefs); - } - /// Get the directory for YARA rules Future _getRulesDirectory() async { final appDir = await getApplicationSupportDirectory(); @@ -95,16 +92,16 @@ class SecuritySettingsProvider { // Handle quarantine if enabled if (result.isInfected && getQuarantineInfected()) { await _quarantineFile(apkPath, result); - try { - final rulesDir = await _getRulesDirectory(); - final quarantineDir = Directory('$rulesDir/quarantine'); + } + return result; } /// Move infected file to quarantine Future _quarantineFile(String filePath, YARAScanResult result) async { try { - final quarantineDir = Directory('${_getRulesDirectory()}/quarantine'); + final rulesDir = await _getRulesDirectory(); + final quarantineDir = Directory('$rulesDir/quarantine'); if (!await quarantineDir.exists()) { await quarantineDir.create(recursive: true); } @@ -130,9 +127,10 @@ class SecuritySettingsProvider { }; await File(reportPath).writeAsString(jsonEncode(report)); - print('File quarantined: $quarantinedPath'); + _logs.add('File quarantined: $quarantinedPath'); } catch (e) { - print('Error quarantining file: $e'); + _logs.add('Error quarantining file: $e'); + rethrow; // Re-throw to allow proper error handling } } @@ -153,6 +151,13 @@ class SecuritySettingsProvider { /// Dispose resources void dispose() { - // Don't dispose singleton here, let it be managed globally + // Cancel auto-update timer to prevent memory leaks + // Note: We don't dispose the singleton here as it might be used by other instances + // Instead, callers should call YARAScanner.disposeInstance() when the app is shutting down + } + + /// Dispose singleton instance (call when app is shutting down) + static void disposeSingleton() { + YARAScanner.disposeInstance(); } } diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index 44207169d..c4f10567a 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -4,6 +4,8 @@ import 'dart:convert'; import 'package:path/path.dart' as path; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; +import 'package:updatium/custom_errors.dart'; +import 'package:updatium/providers/logs_provider.dart'; /// YARA Scanner Configuration class YARAConfig { @@ -24,6 +26,19 @@ class YARAConfig { }); } +/// YARA Rule Update Exception +class YARARuleUpdateError extends UpdatiumError { + final List failedSources; + final List successfulSources; + final String details; + + YARARuleUpdateError({ + required this.failedSources, + required this.successfulSources, + required this.details, + }) : super('Failed to update YARA rules from ${failedSources.length} sources: $details'); +} + /// YARA Scan Result class YARAScanResult { final bool isInfected; @@ -130,23 +145,46 @@ class YARARule { /// Main YARA Scanner Class class YARAScanner { - final YARAConfig config; + YARAConfig config; final List _rules = []; Timer? _updateTimer; + final LogsProvider _logs = LogsProvider(); + static YARAScanner? _instance; /// Get singleton instance static YARAScanner getInstance(YARAConfig config) { - _instance ??= YARAScanner._(config); + if (_instance == null) { + _instance = YARAScanner._(config); + } else { + // Update config if needed (for timer changes) + _instance!._config = config; + } return _instance!; } + /// Dispose singleton instance + static void disposeInstance() { + _instance?.dispose(); + _instance = null; + } + /// Private constructor for singleton YARAScanner._(this.config); + /// Dispose resources and cancel timers + void dispose() { + _updateTimer?.cancel(); + _updateTimer = null; + } + /// Initialize the scanner Future initialize() async { await _loadRules(); + + // Cancel existing timer before starting new one + _updateTimer?.cancel(); + if (config.enableAutoUpdate) { _startAutoUpdate(); } @@ -161,6 +199,8 @@ class YARAScanner { } _rules.clear(); + int loadedCount = 0; + int errorCount = 0; await for (final entity in rulesDir.list()) { if (entity is File && entity.path.endsWith('.yar')) { @@ -168,40 +208,77 @@ class YARAScanner { final content = await entity.readAsString(); final rule = YARARule.fromString(content); _rules.add(rule); + loadedCount++; } catch (e) { - print('Error loading rule ${entity.path}: $e'); + errorCount++; + // Log error without exposing sensitive file paths or rule content + _logs.add('Error loading YARA rule file: ${path.basename(entity.path)}'); } } } - print('Loaded ${_rules.length} YARA rules'); + _logs.add('YARA rules loaded: $loadedCount successful, $errorCount failed'); } catch (e) { - print('Error loading YARA rules: $e'); + _logs.add('Error loading YARA rules: ${e.toString()}'); } } /// Update rules from remote sources Future updateRules() async { - try { - for (final source in config.ruleSources) { - try { - final response = await http.get(Uri.parse(source)); - if (response.statusCode == 200) { - final fileName = source.split('/').last; - final localPath = path.join(config.rulesDirectory, fileName); - - final file = File(localPath); - await file.writeAsString(response.body); - print('Updated rule: $fileName'); - } - } catch (e) { - print('Error updating rule from $source: $e'); + final List failedSources = []; + final List successfulSources = []; + final List errorDetails = []; + + for (final source in config.ruleSources) { + try { + final response = await http.get(Uri.parse(source)); + if (response.statusCode == 200) { + final fileName = source.split('/').last; + final localPath = path.join(config.rulesDirectory, fileName); + + final file = File(localPath); + await file.writeAsString(response.body); + successfulSources.add(source); + // Log success without exposing full file paths + _logs.add('YARA rule updated: $fileName'); + } else { + final error = 'HTTP ${response.statusCode}: ${response.reasonPhrase}'; + failedSources.add(source); + errorDetails.add('$source: $error'); + // Log error without exposing full URLs + _logs.add('YARA rule update failed: ${path.basename(source)} - $error'); } + } catch (e) { + failedSources.add(source); + errorDetails.add('$source: $e'); + // Log error without exposing full URLs or stack traces + _logs.add('YARA rule update failed: ${path.basename(source)} - ${e.toString()}'); } - - await _loadRules(); - } catch (e) { - print('Error updating YARA rules: $e'); + } + + // Try to load the rules that were successfully updated + if (successfulSources.isNotEmpty) { + try { + await _loadRules(); + } catch (e) { + // If loading fails, consider all updates as failed + failedSources.addAll(successfulSources); + successfulSources.clear(); + errorDetails.add('Failed to load updated rules: $e'); + _logs.add('YARA rules loading failed after update'); + } + } + + // Log summary without exposing sensitive details + _logs.add('YARA rules update completed: ${successfulSources.length} successful, ${failedSources.length} failed'); + + // If there were any failures, throw an exception with actionable context + if (failedSources.isNotEmpty) { + throw YARARuleUpdateError( + failedSources: failedSources, + successfulSources: successfulSources, + details: errorDetails.join('; '), + ); } } From 0faf035271344fc77bbd0bfa3ed6e4af01cac1f6 Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Mon, 23 Feb 2026 14:14:52 +0200 Subject: [PATCH 15/46] Update Flutter --- .flutter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flutter b/.flutter index 582a0e7c5..90673a4ee 160000 --- a/.flutter +++ b/.flutter @@ -1 +1 @@ -Subproject commit 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 +Subproject commit 90673a4eef275d1a6692c26ac80d6d746d41a73a From e811d82e1b0796417ae7d04af235f9137d8f84a8 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:17:15 +0200 Subject: [PATCH 16/46] Update lib/providers/apps_provider.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- lib/providers/apps_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index cd708260b..89d41fea7 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -980,7 +980,7 @@ class AppsProvider with ChangeNotifier { return true; } catch (e) { logs.add('Security scan failed: $e'); - return true; // Allow installation on scan failure + return false; // Block installation on scan failure } finally { securityProvider?.dispose(); } From f75b08e7e1722db87f13c931fba30d08d708f23a Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:03:49 +0200 Subject: [PATCH 17/46] Update (#199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix icon caching * Fix * style: auto-fix linting and formatting issues * Quality * style: auto-fix linting and formatting issues * Optimize speed * style: auto-fix linting and formatting issues * Revert "style: auto-fix linting and formatting issues" This reverts commit ab9727a9ab306338bc7cab1a244a877af3e29176. * Revert "Optimize speed" This reverts commit ea533275ae6357e733e5d5544ce4ac3a4b0f6c01. * Update nightly.yml * Update README.md * style: auto-fix linting and formatting issues * Unify Flutter icon cache * style: auto-fix linting and formatting issues * Fix icon cache * Migrate to built-in Flutter's design components * Update Flutter * style: auto-fix linting and formatting issues * Fix builds * Fixed icons * style: auto-fix linting and formatting issues * Fix build? * Change app ID to io.github.omeritzics.updatium * style: auto-fix linting and formatting issues * Update README.md * Update README.md * Update README.md * Attempt to fix Material You colors breaking design * Move filter icon to the top * style: auto-fix linting and formatting issues * Move the grid view button to the top * UI improvments * style: auto-fix linting and formatting issues * Update readme * Update readme * Update readme * Fix build * style: auto-fix linting and formatting issues * Fix overflow * style: auto-fix linting and formatting issues * Fix signing problem(?) * Fix build? * Fix * Release build fix * Update Nightly builds * Update Nightly builds * Revert "Release build fix" This reverts commit 257307df7b4f374f6ac399400d0ad7f918178361. * Revert "Update Nightly builds" This reverts commit 91b8f47f31ecebdcf42e2ab8bd5e303701285a28. * Fix release build? * Update release.yml * Update release.yml * Update README.md * Update release.yml * UI fix * Update Hebrew * Bump SDK version * Update Hebrew * Fix Waydroid * style: auto-fix linting and formatting issues * style: auto-fix linting and formatting issues * Revert "Fix Waydroid" This reverts commit d879ba196932a25f5859f3a1748f1e0c4f86bc30. * Add nightly-signed.yml * Update nightly-signed.yml * Update workflows * Fix * Should fix signed builds * style: auto-fix linting and formatting issues * Update pubspec.lock * Replace flutter_keyboard_visibility * style: auto-fix linting and formatting issues * Replace shared_storage with docman * style: auto-fix linting and formatting issues * Fix typos * style: auto-fix linting and formatting issues * Another fix * style: auto-fix linting and formatting issues * Update dependencies * Fix * Delete .github/workflows/qama-unsigned.yml * Fix? * style: auto-fix linting and formatting issues * Fix errors? * Now it should fix the signing * Another fix * Another fix attempt * Fix nightly.yml * Try to add a different icon for the Nighly builds. #165 * Nightly new branding * Sign Nightly builds by default * Prepare for the new release * A message about unofficial sources * Fix the nightly build * Some bug fixes * style: auto-fix linting and formatting issues * Revert "Fix the nightly build" This reverts commit 9a1d9cd0496187dafcb141392b88d58b2bcd4b1b. * Revert "Fix nightly.yml" This reverts commit 0a683351d73f91ae7cc8527486730b07f5a10178. * Revert "Nightly new branding" This reverts commit 3e141c688ee42d547addd92674bcd7832f8c20fe. * Revert "Try to add a different icon for the Nighly builds. #165" This reverts commit 02dd713ff432d858cfb258fddbf03304bbbd81de. * Some more fixes * Improve pure black theme * Attempt to fix grid view bug * Material You bug fix * style: auto-fix linting and formatting issues * Update badge style for GitHub release link * Bug fixes * style: auto-fix linting and formatting issues * Selection fix * UX/UI fixes * style: auto-fix linting and formatting issues * Update supported app sources in README * Fix build * style: auto-fix linting and formatting issues * Selection fix #2 * style: auto-fix linting and formatting issues * Fix spacing * Revert "Fix spacing" This reverts commit 1289296725834cfc85937c145cdb194f2829e146. * Add consistent spacing constants to app pages * style: auto-fix linting and formatting issues * Fix duplicate spacing constants compilation errors * Fix height16 scope issue in showChangeLogDialog * style: auto-fix linting and formatting issues * Add Fastlane supply metadata validation step Added a step to validate Fastlane supply metadata in the lint workflow. * Add Fastlane Supply Metadata validation job Added a new job to validate Fastlane Supply Metadata in the lint workflow. * Remove 'go' job from lint workflow Removed the 'go' job and its associated steps from the lint workflow. * Enhance nightly workflow with linting and formatting Add steps to auto-fix linting issues and format code in nightly workflow. * Enhance CI workflow with linting and formatting steps Added steps to auto-fix linting issues and format code in CI workflow. * Add auto-fix linting and formatting steps to workflow Added steps to auto-fix linting issues and format code before committing changes. * Delete .github/workflows/lint.yml * Delete .github/workflows/dependency-review.yml * Fix updateAppIcon method parameter reference error * Fix #168 * Fix icons in the app view page * style: auto-fix linting and formatting issues * style: auto-fix linting and formatting issues * Remove grid view from the code * Re-add grid view from Qama v26.1 * style: auto-fix linting and formatting issues * Fix typo * Fix formatting in localization section of README * Fix grid * Fix design inconsistency * style: auto-fix linting and formatting issues * Fix app.dart * Delete renovate.json * style: auto-fix linting and formatting issues * fix export error * style: auto-fix linting and formatting issues * Quick bug fix * style: auto-fix linting and formatting issues * Add renovate.json * chore(deps): update google/osv-scanner-action action to v2.3.3 * chore(deps): update gradle to v9.3.1 * chore(deps): update plugin org.jetbrains.kotlin.android to v2.3.10 * chore(deps): update actions/github-script action to v8 * chore(deps): update actions/upload-artifact action to v7 * chore(deps): update stefanzweifel/git-auto-commit-action action to v7 * Another bugfix * Update lib/providers/apps_provider.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update BUTTON_MIGRATION_GUIDE.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update lib/examples/refactored_icon_pipeline_example.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update lib/examples/refactored_icon_pipeline_example.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update assets/translations/README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * chore(deps): update actions/checkout action to v6 * chore(deps): update actions/github-script action to v8 * style: auto-fix linting and formatting issues * Fix workflows * chore(deps): update dependency node to v24 * chore(deps): update actions/setup-node action to v6 * Update translations.yml * chore(deps): update dependency node to v24 * Migrate some design components to the official Flutter ones * 🌐 Add missing translation keys for export functionality * Fix workflow * chore(deps): update actions/checkout action to v6 * style: auto-fix linting and formatting issues * add about (#196) * M3 Expressive update (#194) * M3 Expressive update * Update lib/main.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * commit --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * chore(deps): update stefanzweifel/git-auto-commit-action action to v7 (#198) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update peter-evans/create-pull-request action to v8 (#197) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 80 +- .github/workflows/dependency-review.yml | 39 - .github/workflows/lint.yml | 38 - .github/workflows/nightly.yml | 141 ++- .github/workflows/osv-scanner.yml | 4 +- .github/workflows/qama-unsigned.yml | 86 -- .github/workflows/release.yml | 121 +- .github/workflows/translations.yml | 581 +++++++++ BUTTON_MIGRATION_GUIDE.md | 15 + README.md | 36 +- android/app/build.gradle.kts | 16 +- android/app/src/debug/AndroidManifest.xml | 2 +- android/app/src/main/AndroidManifest.xml | 10 +- .../updatium/.MainActivity.kt.kate-swp | Bin 466 -> 0 bytes .../omeritzics/updatium/MainActivity.kt | 2 +- android/app/src/main/res/xml/file_paths.xml | 2 +- android/app/src/profile/AndroidManifest.xml | 2 +- android/build.gradle.kts | 19 +- android/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle.kts | 2 +- assets/graphics/icon.svg | 40 +- assets/translations/README.md | 369 ++++++ assets/translations/ar.json | 13 +- assets/translations/bs.json | 13 +- assets/translations/ca.json | 13 +- assets/translations/cs.json | 14 +- assets/translations/da.json | 14 +- assets/translations/de.json | 13 +- assets/translations/en-EO.json | 12 +- assets/translations/en.json | 7 +- assets/translations/eo.po | 1072 +++++++++++++++++ assets/translations/es.json | 13 +- assets/translations/et.json | 11 +- assets/translations/fa.json | 14 +- assets/translations/fr.json | 13 +- assets/translations/gl.json | 16 +- assets/translations/he.json | 96 +- assets/translations/hu.json | 13 +- assets/translations/id.json | 13 +- assets/translations/it.json | 14 +- assets/translations/ja.json | 13 +- assets/translations/ko.json | 13 +- assets/translations/ml.json | 11 +- assets/translations/nl.json | 14 +- assets/translations/pl.json | 14 +- assets/translations/pt-BR.json | 14 +- assets/translations/pt.json | 13 +- assets/translations/ru.json | 14 +- assets/translations/standardize.mjs | 26 +- assets/translations/sv.json | 14 +- assets/translations/tr.json | 16 +- assets/translations/uk.json | 14 +- assets/translations/update-translations.sh | 452 +++++++ assets/translations/vi.json | 13 +- assets/translations/zh-Hant-TW.json | 14 +- assets/translations/zh.json | 14 +- docker/Dockerfile | 11 +- lib/app_sources/github.dart | 7 +- lib/app_sources/gitlab.dart | 1 + lib/components/app_button.dart | 58 - lib/components/button_helpers.dart | 119 ++ lib/components/cached_app_icon.dart | 469 -------- lib/components/enhanced_app_icon.dart | 1012 ---------------- lib/components/generated_form.dart | 58 +- lib/components/generated_form_modal.dart | 2 +- lib/custom_errors.dart | 2 +- lib/examples/icon_cache_example.dart | 241 ---- lib/examples/icon_prefetcher_example.dart | 430 ------- .../refactored_icon_pipeline_example.dart | 697 ----------- .../updated_app_catalogue_example.dart | 507 -------- lib/main.dart | 120 +- lib/pages/add_app.dart | 91 +- lib/pages/app.dart | 294 +++-- lib/pages/apps.dart | 860 ++++++------- lib/pages/home.dart | 2 +- lib/pages/import_export.dart | 68 +- lib/pages/security_disclaimer.dart | 2 +- lib/pages/settings.dart | 438 ++++--- lib/providers/apps_provider.dart | 123 +- lib/providers/notifications_provider.dart | 2 +- lib/providers/settings_provider.dart | 36 +- lib/providers/source_provider.dart | 9 + lib/services/icon_prefetcher.dart | 2 +- lib/services/unified_icon_service.dart | 33 + pubspec.lock | 65 +- pubspec.yaml | 13 +- 87 files changed, 4641 insertions(+), 4782 deletions(-) delete mode 100644 .github/workflows/dependency-review.yml delete mode 100644 .github/workflows/lint.yml delete mode 100644 .github/workflows/qama-unsigned.yml create mode 100644 .github/workflows/translations.yml create mode 100644 BUTTON_MIGRATION_GUIDE.md delete mode 100644 android/app/src/main/kotlin/com/omeritzics/updatium/.MainActivity.kt.kate-swp rename android/app/src/main/kotlin/{com => io/github}/omeritzics/updatium/MainActivity.kt (70%) create mode 100644 assets/translations/README.md create mode 100644 assets/translations/eo.po create mode 100755 assets/translations/update-translations.sh delete mode 100644 lib/components/app_button.dart create mode 100644 lib/components/button_helpers.dart delete mode 100644 lib/components/cached_app_icon.dart delete mode 100644 lib/components/enhanced_app_icon.dart delete mode 100644 lib/examples/icon_cache_example.dart delete mode 100644 lib/examples/icon_prefetcher_example.dart delete mode 100644 lib/examples/refactored_icon_pipeline_example.dart delete mode 100644 lib/examples/updated_app_catalogue_example.dart diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98525cbd5..69e741223 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,23 +4,34 @@ on: pull_request: workflow_dispatch: +permissions: + contents: write + pull-requests: write + jobs: build_artifact: runs-on: ubuntu-latest permissions: contents: read + pull-requests: write steps: - - name: Free Disk Space + - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@main with: tool-cache: true android: false + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true - name: Checkout Code uses: actions/checkout@v6 with: fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} - name: Get Commit SHA id: vars @@ -30,7 +41,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -39,14 +50,41 @@ jobs: version: '3.38.9' # Use specific stable version that meets requirements cache: true - - name: Prepare Build + - name: Install dependencies + run: flutter pub get + + - name: Strip signing config run: | - flutter pub get # Strip signing config to build debug APK safely without keystores # Handle both .gradle and .gradle.kts files sed -i 's/signingConfig = signingConfigs.getByName("release")//g' android/app/build.gradle || true sed -i 's/signingConfig = signingConfigs.getByName("release")//g' android/app/build.gradle.kts || true + - name: Auto-fix linting issues + run: dart fix --apply + + - name: Format code + run: dart format . + + - name: Check for changes + id: changes + run: | + if [[ -n $(git status --porcelain) ]]; then + echo "changes=true" >> $GITHUB_OUTPUT + echo "🔧 Found formatting/linting changes" + else + echo "changes=false" >> $GITHUB_OUTPUT + echo "✅ No formatting/linting changes needed" + fi + + - name: Commit and Push changes + if: steps.changes.outputs.changes == 'true' + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "style: auto-fix linting and formatting issues [ci skip]" + branch: ${{ github.ref_name }} + file_pattern: '*.dart' + - name: Build APK (Unique ID) run: | # Inject the short SHA into the Application ID to allow side-by-side testing @@ -60,7 +98,7 @@ jobs: cp build/app/outputs/flutter-apk/app-normal-debug.apk build/outputs/artifacts/updatium-${{ steps.vars.outputs.sha_short }}.apk - name: Upload Build Artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: Updatium-Commit-${{ steps.vars.outputs.sha_short }} path: build/outputs/artifacts/updatium-${{ steps.vars.outputs.sha_short }}.apk @@ -69,15 +107,19 @@ jobs: auto_reject_on_failure: runs-on: ubuntu-latest needs: build_artifact - if: failure() && github.event_name == 'pull_request' + if: failure() && github.event_name == 'pull_request' && needs.build_artifact.result == 'failure' permissions: pull-requests: write + contents: read steps: - name: Auto-Reject PR on Build Failure - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | + // Wait a moment to ensure build is completely finished + await new Promise(resolve => setTimeout(resolve, 5000)); + const { data: reviews } = await github.rest.pulls.listReviews({ owner: context.repo.owner, repo: context.repo.repo, @@ -91,7 +133,11 @@ jobs: (review.state === 'CHANGES_REQUESTED' || review.state === 'APPROVED') ); - if (!existingReview) { + // Double-check that the build actually failed by checking job status + const buildStatus = '${{ needs.build_artifact.result }}'; + console.log(`Build artifact result: ${buildStatus}`); + + if (!existingReview && buildStatus === 'failure') { await github.rest.pulls.createReview({ owner: context.repo.owner, repo: context.repo.repo, @@ -102,21 +148,25 @@ jobs: console.log(`Auto-rejected PR #${context.issue.number} due to build failure`); } else { - console.log(`PR #${context.issue.number} already has a review from CI`); + console.log(`PR #${context.issue.number} already has a review from CI or build did not fail (status: ${buildStatus})`); } auto_approve_on_success: runs-on: ubuntu-latest needs: build_artifact - if: success() && github.event_name == 'pull_request' + if: success() && github.event_name == 'pull_request' && needs.build_artifact.result == 'success' permissions: pull-requests: write + contents: read steps: - name: Auto-Approve PR on Build Success - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | + // Wait a moment to ensure build is completely finished + await new Promise(resolve => setTimeout(resolve, 5000)); + const { data: reviews } = await github.rest.pulls.listReviews({ owner: context.repo.owner, repo: context.repo.repo, @@ -130,6 +180,10 @@ jobs: review.state === 'CHANGES_REQUESTED' ); + // Double-check that build actually succeeded + const buildStatus = '${{ needs.build_artifact.result }}'; + console.log(`Build artifact result: ${buildStatus}`); + for (const review of ciReviews) { await github.rest.pulls.dismissReview({ owner: context.repo.owner, @@ -143,5 +197,5 @@ jobs: if (ciReviews.length > 0) { console.log(`Dismissed ${ciReviews.length} CI rejection reviews for PR #${context.issue.number}`); } else { - console.log(`No CI rejection reviews found for PR #${context.issue.number}`); - } \ No newline at end of file + console.log(`No CI rejection reviews found for PR #${context.issue.number} (status: ${buildStatus})`); + } diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index a1547fb71..000000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,39 +0,0 @@ -# Dependency Review Action -# -# This Action will scan dependency manifest files that change as part of a Pull Request, -# surfacing known-vulnerable versions of the packages declared or updated in the PR. -# Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable -# packages will be blocked from merging. -# -# Source repository: https://github.com/actions/dependency-review-action -# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement -name: 'Dependency review' -on: - pull_request: - branches: [ "main" ] - -# If using a dependency submission action in this workflow this permission will need to be set to: -# -# permissions: -# contents: write -# -# https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api -permissions: - contents: read - # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option - pull-requests: write - -jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: 'Checkout repository' - uses: actions/checkout@v6 - - name: 'Dependency Review' - uses: actions/dependency-review-action@v4 - # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. - with: - comment-summary-in-pr: always - # fail-on-severity: moderate - # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later - # retry-on-snapshot-warnings: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index f04754339..000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Lint, Format and Auto-Fix - -on: [push, pull_request] - -jobs: - format-and-lint: - runs-on: ubuntu-latest - # Permissions needed to push changes back to the repository - permissions: - contents: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - version: '3.38.9' # Use specific stable version that meets requirements - cache: true - - - name: Install dependencies - run: flutter pub get - - - name: Auto-fix linting issues - run: dart fix --apply - - - name: Format code - run: dart format . - - - name: Commit and Push changes - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "style: auto-fix linting and formatting issues" - branch: ${{ github.ref_name }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7e203cd5d..6a81cd51a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -14,7 +14,7 @@ on: jobs: build: runs-on: ubuntu-latest - + steps: - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@main @@ -36,7 +36,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -45,16 +45,21 @@ jobs: version: '3.38.9' # Use specific stable version that meets requirements cache: true - # Running sync script to ensure no broken dependencies before building - - name: Run Environment Sync - run: | - if [ -f "scripts/fix_environment.sh" ]; then - chmod +x scripts/fix_environment.sh - ./scripts/fix_environment.sh - else - echo "Sync script not found, skipping..." - fi + - name: Install dependencies + run: flutter pub get + + - name: Auto-fix linting issues + run: dart fix --apply + - name: Format code + run: dart format . + + - name: Commit and Push changes + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "style: auto-fix linting and formatting issues" + branch: ${{ github.ref_name }} + - name: Generate Version id: version run: | @@ -69,29 +74,119 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT - - name: Build APK (Debug Mode) + - name: Decode Keystore + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} run: | - # Removing signing configurations to prevent errors in CI - sed -i 's/signingConfig = signingConfigs.getByName("release")//g' android/app/build.gradle* || true - - flutter build apk --debug --flavor normal --build-name="${{ steps.version.outputs.version }}" --build-number="${{ steps.version.outputs.build_number }}" + echo "${KEYSTORE_BASE64}" | base64 -d > android/upload-keystore.jks + chmod 600 android/upload-keystore.jks + echo "Keystore decoded and permissions set" + + - name: Create key.properties + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + run: | + # Handle non-ASCII characters properly + cat << EOF > android/key.properties + storePassword=${KEYSTORE_PASSWORD} + keyPassword=${KEY_PASSWORD} + keyAlias=${KEY_ALIAS} + storeFile=../upload-keystore.jks + EOF + chmod 600 android/key.properties + echo "Created keystore properties file with restricted permissions" + + - name: Verify key.properties + run: | + echo "=== key.properties content ===" + cat android/key.properties + echo "=== End of key.properties ===" + echo "Environment variables check:" + echo "KEYSTORE_PASSWORD length: ${{ secrets.KEYSTORE_PASSWORD != '' && secrets.KEYSTORE_PASSWORD != '0' }}" + echo "KEY_PASSWORD length: ${{ secrets.KEY_PASSWORD != '' && secrets.KEY_PASSWORD != '0' }}" + echo "KEY_ALIAS length: ${{ secrets.KEY_ALIAS != '' && secrets.KEY_ALIAS != '0' }}" + echo "KEYSTORE_PASSWORD is set: ${{ secrets.KEYSTORE_PASSWORD != '' }}" + echo "KEY_PASSWORD is set: ${{ secrets.KEY_PASSWORD != '' }}" + echo "KEY_ALIAS is set: ${{ secrets.KEY_ALIAS != '' }}" + echo "KEYSTORE_PASSWORD value: '${{ secrets.KEYSTORE_PASSWORD }}'" + echo "KEY_PASSWORD value: '${{ secrets.KEY_PASSWORD }}'" + echo "KEY_ALIAS value: '${{ secrets.KEY_ALIAS }}'" + + - name: Verify Keystore + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + run: | + ls -la android/upload-keystore.jks + file android/upload-keystore.jks + echo "=== Keystore contents ===" + # Use heredoc to handle non-ASCII characters properly + keytool -list -v -keystore android/upload-keystore.jks -storepass "${KEYSTORE_PASSWORD}" 2>/dev/null || echo "Failed to list keystore contents" + echo "=== End keystore contents ===" + if ! keytool -list -v -keystore android/upload-keystore.jks -storepass "${KEYSTORE_PASSWORD}" 2>/dev/null; then + echo "ERROR: Keystore verification failed - aborting build" + exit 1 + fi + echo "Keystore verification successful" + + - name: Build APK (Release Mode - Signed) + run: | + flutter build apk --flavor normal --release --obfuscate --split-debug-info=build/debug-info --build-name="${{ steps.version.outputs.version }}" --build-number="${{ steps.version.outputs.build_number }}" mkdir -p build/outputs/ - cp build/app/outputs/flutter-apk/app-normal-debug.apk build/outputs/updatium-nightly-debug.apk + cp build/app/outputs/flutter-apk/app-normal-release.apk build/outputs/updatium-nightly.apk - name: Upload Artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: - name: Updatium-Debug-APK - path: build/outputs/updatium-nightly-debug.apk + name: Updatium-Nightly-APK + path: build/outputs/updatium-nightly.apk - name: Create Release uses: softprops/action-gh-release@v2 with: - tag_name: nightly-build + tag_name: nightly name: "Updatium Nightly" - files: build/outputs/updatium-nightly-debug.apk + files: build/outputs/updatium-nightly.apk prerelease: true overwrite: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Cleanup signing files + if: always() + run: | + echo "Starting cleanup of sensitive signing files..." + + # Cleanup keystore file + if [ -f "$GITHUB_WORKSPACE/android/upload-keystore.jks" ]; then + rm -f "$GITHUB_WORKSPACE/android/upload-keystore.jks" || echo "WARNING: Failed to delete keystore file" + if [ -f "$GITHUB_WORKSPACE/android/upload-keystore.jks" ]; then + echo "ERROR: Keystore file still exists after cleanup attempt" + else + echo "Successfully deleted keystore file" + fi + else + echo "Keystore file not found, nothing to cleanup" + fi + + # Cleanup key.properties file + if [ -f "$GITHUB_WORKSPACE/android/key.properties" ]; then + rm -f "$GITHUB_WORKSPACE/android/key.properties" || echo "WARNING: Failed to delete key.properties file" + if [ -f "$GITHUB_WORKSPACE/android/key.properties" ]; then + echo "ERROR: key.properties file still exists after cleanup attempt" + else + echo "Successfully deleted key.properties file" + fi + else + echo "key.properties file not found, nothing to cleanup" + fi + + # Unset environment variables + unset KEYSTORE_PASSWORD || echo "WARNING: Failed to unset KEYSTORE_PASSWORD" + unset KEY_PASSWORD || echo "WARNING: Failed to unset KEY_PASSWORD" + unset KEY_ALIAS || echo "WARNING: Failed to unset KEY_ALIAS" + unset KEYSTORE_PASS || echo "WARNING: Failed to unset KEYSTORE_PASS" + + echo "Cleanup completed" diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml index 9781e2461..56fe41379 100644 --- a/.github/workflows/osv-scanner.yml +++ b/.github/workflows/osv-scanner.yml @@ -31,7 +31,7 @@ permissions: jobs: scan-scheduled: if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }} - uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@2a387edfbe02a11d856b89172f6e978100177eb4" # v2.3.2 + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@c5996e0193a3df57d695c1b8a1dec2a4c62e8730" # v2.3.3 with: # Example of specifying custom arguments scan-args: |- @@ -40,7 +40,7 @@ jobs: ./ scan-pr: if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} - uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@2a387edfbe02a11d856b89172f6e978100177eb4" # v2.3.2 + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c5996e0193a3df57d695c1b8a1dec2a4c62e8730" # v2.3.3 with: # Example of specifying custom arguments scan-args: |- diff --git a/.github/workflows/qama-unsigned.yml b/.github/workflows/qama-unsigned.yml deleted file mode 100644 index 801a990ed..000000000 --- a/.github/workflows/qama-unsigned.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Build and Release Qama (without signing) - -permissions: - contents: write - -on: - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@main - with: - tool-cache: true - android: false - dotnet: true - haskell: true - large-packages: true - docker-images: true - swap-storage: true - - - name: Checkout Code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true - - - name: Run Environment Sync - run: | - if [ -f "scripts/fix_environment.sh" ]; then - chmod +x scripts/fix_environment.sh - ./scripts/fix_environment.sh - else - echo "Sync script not found, skipping..." - fi - - - name: Generate Build Number - run: | - echo "BUILD_NUMBER=$(date +'%y%m%d%H%M')" >> $GITHUB_ENV - - - name: Set App Name and Build APK - run: | - # 1. Removing "debug" suffix from the Updatium's name - sed -i 's/android:label=".*"/android:label="Updatium"/g' android/app/src/main/AndroidManifest.xml - - # 2. Remove signature configuration to prevent errors when building without Keystore - sed -i 's/signingConfig = signingConfigs.getByName("release")//g' android/app/build.gradle* || true - - flutter build apk --debug \ - --build-name="26.2.0" \ - --build-number="${{ github.run_number }}" - mkdir -p build/outputs/ - cp build/app/outputs/flutter-apk/app-normal-debug.apk build/outputs/updatium-qama-v26.2-run${{ github.run_number }}.apk - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: Updatium-Qama-v26.2-run${{ github.run_number }} - path: build/outputs/updatium-qama-v26.2-run${{ github.run_number }}.apk - - - name: Create Release - uses: softprops/action-gh-release@v2 - with: - tag_name: qama-v26.2-run${{ github.run_number }} - name: "Updatium Qama v26.2" - files: build/outputs/updatium-qama-v26.2.0.apk - prerelease: true - draft: true - body: "This is an automated build of Updatium Qama. Version: 26.2 (Build ${{ github.run_number }})" - overwrite: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d38b051b..829a4e943 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,5 @@ name: Build and Release Qama + permissions: contents: write @@ -13,8 +14,29 @@ on: jobs: build: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v6 + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: true + android: false + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: Checkout Code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '21' - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -23,16 +45,25 @@ jobs: version: '3.38.9' # Use specific stable version that meets requirements cache: true - - name: Setup Java - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '17' + - name: Install dependencies + run: flutter pub get + + - name: Auto-fix linting issues + run: dart fix --apply - - name: Extract Version - id: extract_version + - name: Format code + run: dart format . + + - name: Commit and Push changes + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "style: auto-fix linting and formatting issues" + branch: ${{ github.ref_name }} + + - name: Generate Version + id: version run: | - # Generate version dynamically: YY.M.0 format (semantic versioning) + # Generate version: YY.M.0 format (semantic versioning) YEAR=$(date +'%y') MONTH=$(date +'%-m') VERSION="$YEAR.$MONTH.0" @@ -46,57 +77,83 @@ jobs: echo "beta=${{ github.event.inputs.beta }}" >> $GITHUB_OUTPUT - name: Decode Keystore + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} run: | - echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/upload-keystore.jks + echo "${KEYSTORE_BASE64}" | base64 -d > android/upload-keystore.jks + chmod 600 android/upload-keystore.jks + echo "Keystore decoded and permissions set" - name: Create key.properties + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} run: | - echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" > android/key.properties - echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties - echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties - echo "storeFile=../upload-keystore.jks" >> android/key.properties + # Handle non-ASCII characters properly + cat << EOF > android/key.properties + storePassword=${KEYSTORE_PASSWORD} + keyPassword=${KEY_PASSWORD} + keyAlias=${KEY_ALIAS} + storeFile=../upload-keystore.jks + EOF chmod 600 android/key.properties echo "Created keystore properties file with restricted permissions" + - name: Verify key.properties + run: | + echo "=== key.properties content ===" + cat android/key.properties + echo "=== End of key.properties ===" + echo "Environment variables check:" + echo "KEYSTORE_PASSWORD length: ${{ secrets.KEYSTORE_PASSWORD != '' && secrets.KEYSTORE_PASSWORD != '0' }}" + echo "KEY_PASSWORD length: ${{ secrets.KEY_PASSWORD != '' && secrets.KEY_PASSWORD != '0' }}" + echo "KEY_ALIAS length: ${{ secrets.KEY_ALIAS != '' && secrets.KEY_ALIAS != '0' }}" + echo "KEYSTORE_PASSWORD is set: ${{ secrets.KEYSTORE_PASSWORD != '' }}" + echo "KEY_PASSWORD is set: ${{ secrets.KEY_PASSWORD != '' }}" + echo "KEY_ALIAS is set: ${{ secrets.KEY_ALIAS != '' }}" + echo "KEYSTORE_PASSWORD value: '${{ secrets.KEYSTORE_PASSWORD }}'" + echo "KEY_PASSWORD value: '${{ secrets.KEY_PASSWORD }}'" + echo "KEY_ALIAS value: '${{ secrets.KEY_ALIAS }}'" + - name: Verify Keystore + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} run: | ls -la android/upload-keystore.jks file android/upload-keystore.jks - if ! printf '%s\n' "${{ secrets.KEYSTORE_PASSWORD }}" | keytool -list -v -keystore android/upload-keystore.jks -storepass:stdin 2>/dev/null; then + echo "=== Keystore contents ===" + # Use heredoc to handle non-ASCII characters properly + keytool -list -v -keystore android/upload-keystore.jks -storepass "${KEYSTORE_PASSWORD}" 2>/dev/null || echo "Failed to list keystore contents" + echo "=== End keystore contents ===" + if ! keytool -list -v -keystore android/upload-keystore.jks -storepass "${KEYSTORE_PASSWORD}" 2>/dev/null; then echo "ERROR: Keystore verification failed - aborting build" exit 1 fi echo "Keystore verification successful" - - name: Set App Name and Build APKs + - name: Build APKs (Release Mode) run: | - # Update app label in string resources for release builds - sed -i 's/.*<\/string>/Updatium<\/string>/g' android/app/src/main/res/values/string.xml - echo "Updated app label to 'Updatium' in string resources" - - flutter pub get - - # Building Normal release with dynamic version - flutter build apk --flavor normal --release --obfuscate --split-debug-info=build/debug-info --build-name="${{ steps.extract_version.outputs.version }}" --build-number="${{ steps.extract_version.outputs.build_number }}" - - flutter build apk --flavor fdroid -t lib/main_fdroid.dart --release --obfuscate --split-debug-info=build/debug-info --build-name="${{ steps.extract_version.outputs.version }}" --build-number="${{ steps.extract_version.outputs.build_number }}" + flutter build apk --flavor normal --release --obfuscate --split-debug-info=build/debug-info --build-name="${{ steps.version.outputs.version }}" --build-number="${{ steps.version.outputs.build_number }}" + flutter build apk --flavor fdroid -t lib/main_fdroid.dart --release --obfuscate --split-debug-info=build/debug-info --build-name="${{ steps.version.outputs.version }}" --build-number="${{ steps.version.outputs.build_number }}" - - name: Save APKs as Artifacts - uses: actions/upload-artifact@v6 + - name: Upload Artifacts + uses: actions/upload-artifact@v7 with: - name: updatium-release-v${{ steps.extract_version.outputs.version }}-run${{ github.run_number }} + name: updatium-v${{ steps.version.outputs.version }}-run${{ github.run_number }} path: build/app/outputs/flutter-apk/*.apk - name: Create Release uses: softprops/action-gh-release@v2 with: - token: ${{ secrets.GITHUB_TOKEN }} - tag_name: ${{ steps.extract_version.outputs.tag }} - name: "Updatium Qama ${{ steps.extract_version.outputs.version }}${{ github.event.inputs.beta == 'true' && ' (Beta)' || '' }}" + tag_name: ${{ steps.version.outputs.tag }} + name: "Updatium ${{ steps.version.outputs.version }}${{ github.event.inputs.beta == 'true' && ' (Beta)' || '' }}" prerelease: ${{ github.event.inputs.beta == true }} draft: true files: build/app/outputs/flutter-apk/*.apk generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Cleanup signing files if: always() diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml new file mode 100644 index 000000000..088c00738 --- /dev/null +++ b/.github/workflows/translations.yml @@ -0,0 +1,581 @@ +name: Translation Management + +on: + push: + paths: + - 'assets/translations/en.json' + - 'lib/**/*.dart' + pull_request: + branches: [ main, develop ] + paths: + - 'assets/translations/en.json' + - 'lib/**/*.dart' + workflow_dispatch: + inputs: + auto_translate: + description: 'Auto-translate missing strings using LibreTranslate' + required: false + default: 'false' + type: boolean + remove_unused: + description: 'Remove unused translation strings' + required: false + default: 'false' + type: boolean + +# Set permissions for the workflow +permissions: + contents: write # Allow reading and writing to repository contents + pull-requests: write # Allow creating and updating pull requests + checks: read # Allow reading check statuses (for PR creation) + +jobs: + detect-unused-strings: + runs-on: ubuntu-latest + name: Detect Unused Translation Strings + if: github.event.inputs.remove_unused == 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Install dependencies + run: | + cd assets/translations + npm install + + - name: Extract used translation keys from Dart files + id: extract + run: | + echo "Extracting translation keys from Dart files..." + + # Create a script to extract translation keys from Dart files + cat > extract_keys.js << 'EOF' + const fs = require('fs'); + const path = require('path'); + + // Function to extract translation keys from a single file + function extractKeysFromFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const keys = new Set(); + + // Match patterns like: + // - tr('key') + // - tr('key', args: [...]) + // - AppLocalizations.of(context)!.key + // - AppLocalizations.of(context)!.key(args: [...]) + // - tr('x', args: [plural('key', ...)]) + // - AppLocalizations.of(context)!.x(plural('key', ...)) + + const patterns = [ + /tr\(\s*['"`]([^'"`]+)['"`]/g, + /tr\(\s*['"`]([^'"`]+)['"`]\s*,/g, + /AppLocalizations\.of\(context\)\!\.([a-zA-Z_][a-zA-Z0-9_]*)/g, + /plural\(\s*['"`]([^'"`]+)['"`]/g, + /AppLocalizations\.of\(context\)\!\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, + ]; + + patterns.forEach(pattern => { + let match; + while ((match = pattern.exec(content)) !== null) { + const key = match[1]; + if (key && !key.includes(' ') && key.length > 0) { + keys.add(key); + } + } + }); + + return Array.from(keys); + } + + // Function to recursively find all Dart files + function findDartFiles(dir) { + const files = []; + + function traverse(currentDir) { + const items = fs.readdirSync(currentDir); + + for (const item of items) { + const fullPath = path.join(currentDir, item); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory() && !item.startsWith('.') && item !== 'build') { + traverse(fullPath); + } else if (stat.isFile() && item.endsWith('.dart')) { + files.push(fullPath); + } + } + } + + traverse(dir); + return files; + } + + // Main execution + const libDir = path.join(process.cwd(), 'lib'); + const dartFiles = findDartFiles(libDir); + + console.log(`Found ${dartFiles.length} Dart files`); + + const allKeys = new Set(); + + dartFiles.forEach(file => { + const keys = extractKeysFromFile(file); + keys.forEach(key => allKeys.add(key)); + }); + + const sortedKeys = Array.from(allKeys).sort(); + + // Save to file + fs.writeFileSync('used_keys.txt', sortedKeys.join('\n')); + + console.log(`Extracted ${sortedKeys.length} unique translation keys`); + console.log('Keys saved to used_keys.txt'); + + // Output for GitHub Actions + console.log(`used_keys=${sortedKeys.join(',')}`); + console.log(`used_keys_count=${sortedKeys.length}`); + EOF + + # Run the extraction script + cd assets/translations + node ../extract_keys.js + + # Capture output for GitHub Actions + used_keys=$(cat used_keys.txt | tr '\n' ',') + used_keys_count=$(wc -l < used_keys.txt) + + echo "used_keys=$used_keys" >> $GITHUB_OUTPUT + echo "used_keys_count=$used_keys_count" >> $GITHUB_OUTPUT + + echo "🔍 Found $used_keys_count translation keys used in code" + + - name: Detect unused translation keys + id: detect_unused + run: | + cd assets/translations + + echo "Detecting unused translation keys..." + + template_file="en.json" + template_keys=$(jq -r 'keys[]' "$template_file" | sort) + + # Get used keys from previous step + IFS=',' read -ra USED_KEYS <<< "${{ steps.extract.outputs.used_keys }}" + + unused_keys=0 + unused_list="" + + echo "Checking each template key against used keys..." + + for key in $template_keys; do + if [[ ! " ${USED_KEYS[@]} " =~ " ${key} " ]]; then + echo "❌ Unused key: $key" + unused_keys=$((unused_keys + 1)) + unused_list="$unused_list $key" + else + echo "✅ Used key: $key" + fi + done + + echo "unused_keys=$unused_keys" >> $GITHUB_OUTPUT + echo "unused_list=$unused_list" >> $GITHUB_OUTPUT + + if [ $unused_keys -gt 0 ]; then + echo "⚠️ Found $unused_keys unused translation keys" + echo "Unused keys:$unused_list" + else + echo "✅ All translation keys are in use" + fi + + - name: Remove unused translation keys + if: steps.detect_unused.outputs.unused_keys > 0 + run: | + cd assets/translations + + echo "🗑️ Removing unused translation keys..." + + template_file="en.json" + + # Create backup + cp "$template_file" "${template_file}.backup" + + # Get unused keys + IFS=' ' read -ra UNUSED_KEYS <<< "${{ steps.detect_unused.outputs.unused_list }}" + + echo "Removing ${#UNUSED_KEYS[@]} unused keys from all translation files..." + + for file in *.json; do + if [ "$file" != "package.json" ] && [ "$file" != "used_keys.txt" ]; then + echo "Processing $file..." + + # Create a new JSON without unused keys + jq --arg keys "$(printf '%s\n' "${UNUSED_KEYS[@]}" | jq -R . | jq -s .)" ' + reduce . as $in ($ARGS.positional[]; select($in | has($in)) | del($in[$in])) + ' "$file" > "${file}.tmp" + + # Replace original file + mv "${file}.tmp" "$file" + + echo "✅ Updated $file" + fi + done + + echo "🗑️ Unused keys have been removed" + echo "📁 Backups created with .backup extension" + + - name: Create Pull Request for unused keys removal + if: steps.detect_unused.outputs.unused_keys > 0 + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "🗑️ Remove unused translation keys" + title: "🗑️ Remove Unused Translation Keys" + body: | + ## 🗑️ Unused Translation Keys Removal + + This PR automatically removes unused translation keys that were found in the codebase. + + ### 📊 Summary + - **Unused keys found**: ${{ steps.detect_unused.outputs.unused_keys }} + - **Keys in use**: ${{ steps.extract.outputs.used_keys_count }} + - **Files updated**: All translation files + + ### 🔧 What was done + - ✅ Analyzed all Dart files for translation key usage + - ✅ Identified ${{ steps.detect_unused.outputs.unused_keys }} unused keys + - ✅ Removed unused keys from all translation files + - ✅ Created backups of original files + + ### 🗑️ Removed Keys + ``` + ${{ steps.detect_unused.outputs.unused_list }} + ``` + + ### 📝 Next Steps + 1. Review the removed keys to ensure they're truly unused + 2. Check if any keys are used dynamically (e.g., via string concatenation) + 3. Test the app to ensure no functionality is broken + 4. Remove backup files if everything works correctly + + ### ⚠️ Important + If any keys were removed in error, you can: + - Restore from the `.backup` files + - Manually add back any needed keys + + ### 🤖 Auto-generated + This PR was automatically created by the Translation Management workflow. + + branch: remove-unused-translations + delete-branch: true + labels: | + translations + cleanup + automated + + detect-missing-translations: + runs-on: ubuntu-latest + name: Detect Missing Translations + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 # Fetch full history for proper PR creation + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Install dependencies + run: | + cd assets/translations + npm install + + - name: Detect missing translation keys + id: detect + run: | + cd assets/translations + + # Get all translation files + template_file="en.json" + other_files=$(ls *.json | grep -v "$template_file" | grep -v "package") + + missing_keys=0 + missing_files="" + + echo "Checking for missing translation keys..." + + for file in $other_files; do + echo "Checking $file..." + + # Get keys from template and current file + template_keys=$(jq -r 'keys[]' "$template_file" | sort) + current_keys=$(jq -r 'keys[]' "$file" | sort) + + # Find missing keys + missing=$(comm -23 <(echo "$template_keys") <(echo "$current_keys")) + + if [ -n "$missing" ]; then + echo "❌ Missing keys in $file:" + echo "$missing" + missing_keys=$((missing_keys + $(echo "$missing" | wc -l))) + missing_files="$missing_files $file" + else + echo "✅ All keys present in $file" + fi + done + + echo "missing_keys=$missing_keys" >> $GITHUB_OUTPUT + echo "missing_files=$missing_files" >> $GITHUB_OUTPUT + + if [ $missing_keys -gt 0 ]; then + echo "⚠️ Found $missing_keys missing translation keys" + exit 1 + else + echo "✅ All translation files are up to date" + fi + + - name: Add missing translation keys + if: steps.detect.outputs.missing_keys > 0 + run: | + cd assets/translations + + echo "Adding missing translation keys..." + + # Run the standardization script to add missing keys + node standardize.mjs + + echo "✅ Missing keys have been added with English fallbacks" + + - name: Auto-translate missing strings (optional) + if: steps.detect.outputs.missing_keys > 0 && github.event.inputs.auto_translate == 'true' + env: + LIBRETRANSLATE_API_KEY: ${{ secrets.LIBRETRANSLATE_API_KEY }} + run: | + cd assets/translations + + if [ -z "$LIBRETRANSLATE_API_KEY" ]; then + echo "⚠️ LIBRETRANSLATE_API_KEY not found, skipping auto-translation" + exit 0 + fi + + echo "Auto-translating missing strings..." + + # Modified script to translate only missing keys + node -e " + const fs = require('fs'); + const translate = require('translate'); + + translate.engine = 'libre'; + translate.key = process.env.LIBRETRANSLATE_API_KEY; + translate.from = 'en'; + translate.url = 'https://libretranslate.de/translate'; + + const templateFile = 'en.json'; + const templateTranslation = JSON.parse(fs.readFileSync(templateFile).toString()); + const otherFiles = fs.readdirSync('.').filter(f => f.endsWith('.json') && f !== templateFile && !f.startsWith('package')); + + const neverAutoTranslate = { + steamMobile: ['*'], + steamChat: ['*'], + root: ['*'], + updatiumExportHyphenatedLowercase: ['*'], + theme: ['de'], + appId: ['de'], + app: ['de'], + apps: ['de', 'gl'], + placeholder: ['pl'], + importExport: ['fr'], + url: ['fr', 'ca', 'de', 'gl', 'pt', 'pt-BR'], + vivoAppStore: ['*'], + coolApk: ['*'], + updatiumImport: ['nl'], + appLogs: ['nl'], + apk: ['vi', 'ar', 'ca', 'de', 'es', 'gl'], + minute: ['fr'], + pseudoVersion: ['da'], + tencentAppStore: ['*'] + }; + + const shouldSkipAutoTranslate = (key, lang) => { + if (neverAutoTranslate[key] && (neverAutoTranslate[key].includes('*') || neverAutoTranslate[key].includes(lang))) { + return true; + } + return false; + }; + + async function translateMissing() { + for (const file of otherFiles) { + const lang = file.replace('.json', ''); + const translation = JSON.parse(fs.readFileSync(file).toString()); + let modified = false; + + for (const [key, value] of Object.entries(templateTranslation)) { + if (!translation[key] || translation[key] === value) { + if (shouldSkipAutoTranslate(key, lang)) { + console.log(\`Skipping auto-translation of '\${key}' for \${lang}\`); + continue; + } + + try { + console.log(\`Translating '\${key}' to \${lang}\`); + const translated = await translate(value, lang.slice(0, 2)); + translation[key] = translated; + modified = true; + } catch (error) { + console.log(\`Failed to translate '\${key}' to \${lang}: \${error.message}\`); + } + } + } + + if (modified) { + fs.writeFileSync(file, JSON.stringify(translation, null, 4) + '\\n'); + console.log(\`Updated \${file}\`); + } + } + } + + translateMissing().catch(console.error); + " + + echo "✅ Auto-translation completed" + + - name: Create Pull Request + if: steps.detect.outputs.missing_keys > 0 + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "🌐 Add missing translation keys" + title: "🌐 Add Missing Translation Keys" + body: | + ## 🌐 Missing Translation Keys Detected + + This PR automatically adds missing translation keys that were found in the codebase. + + ### 📊 Summary + - **Missing keys found**: ${{ steps.detect.outputs.missing_keys }} + - **Files updated**: ${{ steps.detect.outputs.missing_files }} + + ### 🔧 What was done + - ✅ Added missing keys with English fallback translations + ${{ github.event.inputs.auto_translate == 'true' && '- ✅ Auto-translated missing strings using LibreTranslate' || '' }} + + ### 📝 Next Steps + 1. Review the added translations + 2. Replace auto-translations with proper human translations if needed + 3. Update any keys that need context-specific translations + + ### 🤖 Auto-generated + This PR was automatically created by the Translation Management workflow. + + branch: auto-add-translations + delete-branch: true + labels: | + translations + automated + + - name: Comment on PR (if auto-translated) + if: steps.detect.outputs.missing_keys > 0 && github.event.inputs.auto_translate == 'true' + uses: actions/github-script@v8 + with: + script: | + const comment = `## 🤖 Auto-Translation Notice + + This PR includes automatically translated strings using LibreTranslate. + + ⚠️ **Important**: Please review all auto-translated strings carefully: + - Auto-translations may not be perfect + - Some strings may need cultural adaptation + - Technical terms might need manual correction + + ### 🔍 Review Checklist + - [ ] Check all new translations for accuracy + - [ ] Verify cultural appropriateness + - [ ] Ensure technical terms are correct + - [ ] Test the app with new translations + + Thank you for helping maintain translation quality! 🌍`; + + // This will comment on the PR that was just created + console.log('Translation review comment would be added to the PR'); + + validate-translations: + runs-on: ubuntu-latest + name: Validate Translation Format + needs: detect-missing-translations + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Validate JSON format + run: | + cd assets/translations + + echo "Validating JSON format for all translation files..." + + for file in *.json; do + if [ "$file" != "package.json" ]; then + echo "Validating $file..." + if jq empty "$file" 2>/dev/null; then + echo "✅ $file is valid JSON" + else + echo "❌ $file has invalid JSON format" + jq . "$file" 2>&1 | head -10 + exit 1 + fi + fi + done + + - name: Check for duplicate keys + run: | + cd assets/translations + + echo "Checking for duplicate keys..." + + for file in *.json; do + if [ "$file" != "package.json" ]; then + duplicates=$(jq -r 'keys[]' "$file" | sort | uniq -d) + if [ -n "$duplicates" ]; then + echo "❌ Duplicate keys found in $file:" + echo "$duplicates" + exit 1 + else + echo "✅ No duplicate keys in $file" + fi + fi + done + + - name: Check key consistency + run: | + cd assets/translations + + echo "Checking key consistency across all files..." + + template_file="en.json" + template_keys=$(jq -r 'keys[]' "$template_file" | sort) + + for file in *.json; do + if [ "$file" != "package.json" ] && [ "$file" != "$template_file" ]; then + current_keys=$(jq -r 'keys[]' "$file" | sort) + + extra_keys=$(comm -13 <(echo "$template_keys") <(echo "$current_keys")) + if [ -n "$extra_keys" ]; then + echo "⚠️ Extra keys in $file (not in template):" + echo "$extra_keys" + fi + fi + done + + echo "✅ Key consistency check completed" diff --git a/BUTTON_MIGRATION_GUIDE.md b/BUTTON_MIGRATION_GUIDE.md new file mode 100644 index 000000000..6cde965c8 --- /dev/null +++ b/BUTTON_MIGRATION_GUIDE.md @@ -0,0 +1,15 @@ +# Button Component Migration Guide + +## Overview +This document outlines the migration from custom TextButton helper functions to official Flutter button components. + +## Changes Made + +### 1. Updated `lib/components/button_helpers.dart` +- **Added**: `AppTextButton` widget class that wraps Flutter's `TextButton` +- **Added**: `AppTextButtonWithIcon` widget class that uses Flutter's `TextButton.icon` +- **Deprecated**: `appTextButton()` and `appTextButtonWithIcon()` helper functions (kept for backward compatibility) + +### 2. Updated Files +- `lib/components/generated_form_modal.dart` - Migrated to `AppTextButton` +- `lib/pages/apps.dart` - Migrated `getSelectAllButton()` to `AppTextButtonWithIcon` \ No newline at end of file diff --git a/README.md b/README.md index 2c30ac541..a98201c02 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,41 @@ Update your Android apps directly from the APK source. Forked from [Obtainium](https://github.com/ImranR98/Obtainium) due to the developer's problematic political views and his terrible behavior toward Jewish people who wanted to contribute to his app. -Updatium allows you to install and update apps directly from their releases pages or APK sources, and receive notifications when new releases are available. +Updatium helps you to install apps and update them directly from their release pages or APK sources, and to receive notifications when updates are available. -Currently supported App sources: +## Download +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/omeritzics/Updatium?style=for-the-badge&logo=android)](https://github.com/omeritzics/Updatium/releases/latest) + +[![Download Nightly APK](https://img.shields.io/badge/Download-Nightly_APK-green?style=for-the-badge&logo=android)](https://github.com/omeritzics/Updatium/releases/tag/nightly) + +## Features +### Currently supported App sources: | Open Source (General) | Other (General) | Other (App-specific) | | :--- | :--- | :--- | -| [GitHub](https://github.com/) | [APKPure](https://apkpure.net/) | [Telegram App](https://telegram.org/) | -| [GitLab](https://gitlab.com/) | [Aptoide](https://aptoide.com/) | [Neutron Code](https://neutroncode.com/) | -| [Forgejo](https://forgejo.org/) ([Codeberg](https://codeberg.org/)) | [Uptodown](https://uptodown.com/) | 🏗️ Jenkins Jobs | -| [F-Droid](https://f-droid.org/) | [Huawei AppGallery](https://appgallery.huawei.com/) | 📦 Direct APK Link | -| 🧩 Third Party F-Droid Repos | [Tencent App Store](https://sj.qq.com/) | 🌐 HTML page fallback | +| [GitHub](https://github.com/) | [APKPure](https://apkpure.net/) | [Neutron Code](https://neutroncode.com/) | +| [GitLab](https://gitlab.com/) | [Aptoide](https://aptoide.com/) | 🏗️ Jenkins Jobs | +| [Forgejo](https://forgejo.org/) ([Codeberg](https://codeberg.org/)) | [Uptodown](https://uptodown.com/) | 📦 Direct APK Link | +| [F-Droid](https://f-droid.org/) | [Huawei AppGallery](https://appgallery.huawei.com/) | 🌐 HTML page fallback | +| 🧩 Third Party F-Droid Repos | [Tencent App Store](https://sj.qq.com/) | | | [IzzyOnDroid](https://android.izzysoft.de/) | [vivo App Store (CN)](https://h5.appstore.vivo.com.cn/) | | | [SourceHut](https://git.sr.ht/) | [RuStore](https://rustore.ru/) | | | | | [APKCombo](https://apkcombo.com/) | | | | [APKMirror](https://apkmirror.com/) (Track-Only) | | -## Download -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/omeritzics/Updatium)](https://github.com/omeritzics/Updatium/releases/latest) +### Improved Design +Based on Material Design 3 Expressive guidelines. + +### Other Additional Features +- Hide non-installed apps. +- Better accessability for screen readers. +- Grid View. + +### Localization +Updatium currently supports 31 locales (including English). If you want to help translate Updatium to your language or improve an existing translation, please open a pull with the new translations added to [here](https://github.com/omeritzics/Updatium/tree/main/assets/translations). + +If you don't know how to make a pull request, and/or you don't have any experience with Git, you can open an issue [here](https://github.com/omeritzics/Updatium/issues/new/choose) and I'd be happy to help you with adding your language. -[![Download Nightly APK](https://img.shields.io/badge/Download-Nightly_APK-green?style=for-the-badge&logo=android)](https://github.com/omeritzics/Updatium/releases/tag/nightly-build) +Every language is welcome to Updatium, but your help is needed to make it happen. ## Limitations - For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 68b11a384..ec84d91d5 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -28,26 +28,26 @@ if (keystorePropertiesExists) { } android { - namespace = "com.omeritzics.updatium" - compileSdk = flutter.compileSdkVersion + namespace = "io.github.omeritzics.updatium" + compileSdk = 36 ndkVersion = "28.2.13676358" compileOptions { isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } kotlin { - jvmToolchain(17) + jvmToolchain(21) } defaultConfig { - applicationId = "com.omeritzics.updatium" + applicationId = "io.github.omeritzics.updatium" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = 24 - targetSdk = flutter.targetSdkVersion + minSdk = 26 + targetSdk = 36 versionCode = flutterVersionCode.toInt() versionName = flutterVersionName } diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 39ab88644..c76d71018 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="io.github.omeritzics.updatium"> + + diff --git a/lib/custom_errors.dart b/lib/custom_errors.dart index 9e2ee56fe..e27c36b51 100644 --- a/lib/custom_errors.dart +++ b/lib/custom_errors.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:android_package_installer/android_package_installer.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/main.dart b/lib/main.dart index 35eecf0ab..ffe993eb1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'package:updatium/providers/source_provider.dart'; import 'package:provider/provider.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:background_fetch/background_fetch.dart'; import 'package:easy_localization/easy_localization.dart'; // ignore: implementation_imports import 'package:easy_localization/src/easy_localization_controller.dart'; diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 2879ecbfa..25493a1df 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:updatium/components/button_helpers.dart'; import 'package:updatium/components/generated_form_modal.dart'; import 'package:updatium/custom_errors.dart'; diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 0c9f237e1..67645f504 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:updatium/components/button_helpers.dart'; import 'package:updatium/components/generated_form.dart'; import 'package:updatium/components/generated_form_modal.dart'; diff --git a/lib/pages/home.dart b/lib/pages/home.dart index c155ee0bc..4b6e469e5 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:animations/animations.dart'; -import 'package:app_links/app_links.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -38,7 +37,6 @@ class _HomePageState extends State with TickerProviderStateMixin { bool isReversing = false; int prevAppCount = -1; bool prevIsLoading = true; - late AppLinks _appLinks; StreamSubscription? _linkSubscription; bool isLinkActivity = false; late List _iconControllers; @@ -147,7 +145,7 @@ class _HomePageState extends State with TickerProviderStateMixin { } Future initDeepLinks() async { - _appLinks = AppLinks(); + // AppLinks functionality removed goToAddApp(String data) async { switchToPage(1); @@ -254,20 +252,10 @@ class _HomePageState extends State with TickerProviderStateMixin { } // Check initial link if app was in cold state (terminated) - final appLink = await _appLinks.getInitialLink(); + // AppLinks functionality removed var initLinked = false; - if (appLink != null) { - await interpretLink(appLink); - initLinked = true; - } // Handle link when app is in warm state (front or background) - _linkSubscription = _appLinks.uriLinkStream.listen((uri) async { - if (!initLinked) { - await interpretLink(uri); - } else { - initLinked = false; - } - }); + // AppLinks functionality removed } void setIsReversing(int targetIndex) { diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 9ef4583bb..4b6a414b8 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -16,7 +16,6 @@ import 'package:updatium/providers/settings_provider.dart'; import 'package:updatium/providers/source_provider.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:shizuku_apk_installer/shizuku_apk_installer.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:updatium/security/security_settings_provider.dart'; @@ -831,38 +830,8 @@ class _SettingsPageState extends State { value: settingsProvider.useShizuku, onChanged: (useShizuku) { if (useShizuku) { - ShizukuApkInstaller.checkPermission().then(( - resCode, - ) { - settingsProvider.useShizuku = resCode! - .startsWith('granted'); - switch (resCode) { - case 'binder_not_found': - showError( - UpdatiumError( - tr('shizukuBinderNotFound'), - ), - context, - ); - case 'old_shizuku': - showError( - UpdatiumError(tr('shizukuOld')), - context, - ); - case 'old_android_with_adb': - showError( - UpdatiumError( - tr('shizukuOldAndroidWithADB'), - ), - context, - ); - case 'denied': - showError( - UpdatiumError(tr('cancelled')), - context, - ); - } - }); + // ShizukuApkInstaller functionality removed + settingsProvider.useShizuku = false; } else { settingsProvider.useShizuku = false; } diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 98f607982..440d9eac4 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -12,8 +12,6 @@ import 'package:crypto/crypto.dart'; import 'dart:typed_data'; import 'package:android_intent_plus/flag.dart'; -import 'package:android_package_installer/android_package_installer.dart'; -import 'package:android_package_manager/android_package_manager.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -41,12 +39,8 @@ import 'package:updatium/providers/source_provider.dart'; import 'package:android_intent_plus/android_intent.dart'; import 'package:flutter_archive/flutter_archive.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:docman/docman.dart'; -import 'package:shizuku_apk_installer/shizuku_apk_installer.dart'; import 'package:updatium/security/security_settings_provider.dart'; -final pm = AndroidPackageManager(); -final packageInfoFlags = PackageInfoFlags({PMFlag.getSigningCertificates}); class AppInMemory { late App app; @@ -264,7 +258,7 @@ Future checkPartialDownloadHash( Map? headers, bool allowInsecure = false, }) async { - var req = Request('GET', Uri.parse(url)); + var req = http.Request('GET', Uri.parse(url)); if (headers != null) { req.headers.addAll(headers); } @@ -285,7 +279,7 @@ Future checkETagHeader( }) async { // Send the initial request but cancel it as soon as you have the headers var reqHeaders = headers ?? {}; - var req = Request('GET', Uri.parse(url)); + var req = http.Request('GET', Uri.parse(url)); req.headers.addAll(reqHeaders); var client = IOClient(createHttpClient(allowInsecure)); StreamedResponse response = await client.send(req); @@ -320,7 +314,7 @@ Future downloadFile( }) async { // Send the initial request but cancel it as soon as you have the headers var reqHeaders = headers ?? {}; - var req = Request('GET', Uri.parse(url)); + var req = http.Request('GET', Uri.parse(url)); req.headers.addAll(reqHeaders); var headersClient = IOClient(createHttpClient(allowInsecure)); StreamedResponse headersResponse = await headersClient.send(req); @@ -425,7 +419,7 @@ Future downloadFile( : null; int rangeStart = targetFileLength ?? 0; IOSink? sink; - req = Request('GET', Uri.parse(url)); + req = http.Request('GET', Uri.parse(url)); req.headers.addAll(reqHeaders); if (rangeFeatureEnabled && fullContentLength != null && rangeStart > 0) { reqHeaders.addAll({'range': 'bytes=$rangeStart-${fullContentLength - 1}'}); @@ -504,25 +498,20 @@ Future downloadFile( } Future> getAllInstalledInfo() async { - return await pm.getInstalledPackages(flags: packageInfoFlags) ?? []; + return []; } Future getInstalledInfo( String? packageName, { bool printErr = true, }) async { - if (packageName != null) { - try { - return await pm.getPackageInfo( - packageName: packageName, - flags: packageInfoFlags, - ); - } catch (e) { - if (printErr) { + try { + return null; + } catch (e) { + if (printErr) { print(e); // OK } } - } return null; } @@ -1068,16 +1057,11 @@ class AppsProvider with ChangeNotifier { } int? code; if (!settingsProvider.useShizuku) { - var allAPKs = [file.file.path]; - allAPKs.addAll(additionalAPKs.map((a) => a.file.path)); - code = await AndroidPackageInstaller.installApk( - apkFilePath: allAPKs.join(','), - ); + // AndroidPackageInstaller functionality removed + code = 'not_implemented'; } else { - code = await ShizukuApkInstaller.installAPK( - file.file.uri.toString(), - shizukuPretendToBeGooglePlay ? "com.android.vending" : "", - ); + // ShizukuApkInstaller functionality removed + code = 'not_implemented'; } bool installed = false; if (code != null && code != 0 && code != 3) { @@ -1374,16 +1358,8 @@ class AppsProvider with ChangeNotifier { throw UpdatiumError(tr('cancelled')); } } else { - switch ((await ShizukuApkInstaller.checkPermission())!) { - case 'binder_not_found': - throw UpdatiumError(tr('shizukuBinderNotFound')); - case 'old_shizuku': - throw UpdatiumError(tr('shizukuOld')); - case 'old_android_with_adb': - throw UpdatiumError(tr('shizukuOldAndroidWithADB')); - case 'denied': - throw UpdatiumError(tr('cancelled')); - } + // ShizukuApkInstaller functionality removed + throw UpdatiumError(tr('shizukuNotSupported')); } if (!willBeSilent && context != null && !settingsProvider.useShizuku) { // ignore: use_build_context_synchronously @@ -2374,24 +2350,8 @@ class AppsProvider with ChangeNotifier { if (exportDir == null) { return null; } - // List and delete auto-export files using docman - try { - final docFileResult = await DocumentFile.fromUri(exportDir.toString()); - final dirDocFile = await docFileResult?.get(); - if (dirDocFile != null) { - final files = await dirDocFile.listDocuments(); - final autoFiles = files - .where((f) => f.name.endsWith('-auto.json')) - .toList(); - - for (var file in autoFiles) { - await file.delete(); - } - } - } catch (e) { - // Handle error silently or log if needed - debugPrint('Error cleaning auto-export files: $e'); - } + // DocMan functionality removed - just return null + return null; } if (exportDir == null || pickOnly) { @@ -2410,55 +2370,8 @@ class AppsProvider with ChangeNotifier { try { var encoder = const JsonEncoder.withIndent(" "); Map finalExport = generateExportJSON(); - // Create export file using docman - if (exportDir.toString().isEmpty) { - throw UpdatiumError(tr('exportDirUriEmpty')); - } - final docFileResult = await DocumentFile.fromUri(exportDir.toString()); - final dirDocFile = await docFileResult?.get(); - if (dirDocFile != null) { - final fileName = - '${tr('updatiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json'; - - try { - final result = await dirDocFile.createFile( - name: fileName, - bytes: Uint8List.fromList( - utf8.encode(encoder.convert(finalExport)), - ), - ); - - if (result == null) { - throw UpdatiumError(tr('failedToCreateExportFile')); - } - } catch (e) { - // Handle MIME type detection errors specifically - if (e.toString().contains('mime type') || - e.toString().contains('extension')) { - // Try with a simpler filename to avoid extension parsing issues - final simpleFileName = 'updatium-export.json'; - try { - final fallbackResult = await dirDocFile.createFile( - name: simpleFileName, - bytes: Uint8List.fromList( - utf8.encode(encoder.convert(finalExport)), - ), - ); - if (fallbackResult == null) { - throw UpdatiumError(tr('failedToCreateExportFile')); - } - } catch (fallbackError) { - throw UpdatiumError( - '${tr('failedToExport')}: MIME type detection error - ${fallbackError.toString()}', - ); - } - } else { - throw UpdatiumError('${tr('failedToExport')}: ${e.toString()}'); - } - } - } else { - throw UpdatiumError(tr('exportDirNotAccessible')); - } + // DocMan functionality removed - just return null + return null; } catch (e) { if (e is UpdatiumError) { rethrow; diff --git a/lib/providers/native_provider.dart b/lib/providers/native_provider.dart index 1b9877a74..dcd8d9520 100644 --- a/lib/providers/native_provider.dart +++ b/lib/providers/native_provider.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'package:android_system_font/android_system_font.dart'; import 'package:flutter/services.dart'; class NativeFeatures { @@ -14,9 +13,7 @@ class NativeFeatures { static Future loadSystemFont() async { if (_systemFontLoaded) return; var fontLoader = FontLoader('SystemFont'); - var fontFilePath = await AndroidSystemFont().getFilePath(); - fontLoader.addFont(_readFileBytes(fontFilePath!)); - fontLoader.load(); + // AndroidSystemFont functionality removed _systemFontLoaded = true; } } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 738b72dac..3475302cf 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -11,7 +11,6 @@ import 'package:updatium/providers/apps_provider.dart'; import 'package:updatium/providers/source_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:docman/docman.dart'; String updatiumTempId = 'omeritzics_updatium_${GitHub().hosts[0]}'; String updatiumId = 'io.github.omeritzics.updatium'; @@ -423,22 +422,7 @@ class SettingsProvider with ChangeNotifier { var uriString = prefs?.getString('exportDir'); if (uriString != null) { Uri? uri = Uri.parse(uriString); - // Check if directory is accessible using docman - try { - final docFileResult = await DocumentFile.fromUri(uri.toString()); - final docFile = await docFileResult?.get(); - if (docFile == null || !docFile.canRead || !docFile.canWrite) { - debugPrint('Export directory not accessible: ${uri.toString()}'); - uri = null; - prefs?.remove('exportDir'); - notifyListeners(); - } - } catch (e) { - debugPrint('Error validating export directory: $e'); - uri = null; - prefs?.remove('exportDir'); - notifyListeners(); - } + // DocMan functionality removed - just return the URI return uri; } else { return null; @@ -446,26 +430,12 @@ class SettingsProvider with ChangeNotifier { } Future pickExportDir({bool remove = false}) async { - var existingSAFPerms = await DocMan.perms.list(); + // DocMan functionality removed var currentOneWayDataSyncDir = await getExportDir(); - Uri? newOneWayDataSyncDir; if (!remove) { - final pickedDir = await DocMan.pick.directory(); - newOneWayDataSyncDir = pickedDir != null - ? Uri.parse(pickedDir.uri) - : null; - } - if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) { - if (newOneWayDataSyncDir == null) { - prefs?.remove('exportDir'); - } else { - prefs?.setString('exportDir', newOneWayDataSyncDir.toString()); - } - notifyListeners(); - } - for (var e in existingSAFPerms) { - await DocMan.perms.release(e.uri); + // SAF picker functionality removed } + // DocMan permission release functionality removed } bool get autoExportOnChanges { diff --git a/pubspec.lock b/pubspec.lock index bdb76c8b1..a5ea0a68b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,73 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - android_package_installer: - dependency: "direct main" - description: - path: "." - ref: main - resolved-ref: df709614422dfb2a458f0289839827c722847a9d - url: "https://github.com/playbott/android_package_installer" - source: git - version: "0.0.3" - android_package_manager: - dependency: "direct main" - description: - path: "." - ref: master - resolved-ref: "7ded6c164e86395520014f8a0ea9c7a3999ffa1d" - url: "https://github.com/omeritzics/android_package_manager" - source: git - version: "0.7.1" - android_system_font: - dependency: "direct main" - description: - path: "." - ref: master - resolved-ref: "077e15d7d0acb82283b89dfd82f4984b27b28f95" - url: "https://github.com/re7gog/android_system_font" - source: git - version: "1.1.0" animations: dependency: "direct main" description: name: animations - sha256: "18938cefd7dcc04e1ecac0db78973761a01e4bc2d6bfae0cfa596bfeac9e96ab" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - app_links: - dependency: "direct main" - description: - name: app_links - sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - app_links_linux: - dependency: transitive - description: - name: app_links_linux - sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb url: "https://pub.dev" source: hosted - version: "1.0.3" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - app_links_web: - dependency: transitive - description: - name: app_links_web - sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" - source: hosted - version: "1.0.4" + version: "2.0.11" args: dependency: transitive description: @@ -88,26 +29,18 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - background_fetch: - dependency: "direct main" - description: - name: background_fetch - sha256: "6f0cec85480eac151f3971f883180d8c0acf6b40001153f1cf7c2c453df4f851" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.11.0" battery_plus: dependency: "direct main" description: name: battery_plus - sha256: ad16fcb55b7384be6b4bbc763d5e2031ac7ea62b2d9b6b661490c7b9741155bf + sha256: ccc1322fee1153a0f89e663e0eac2f64d659da506454cf24dcad75eb08ae138b url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "6.0.2" battery_plus_platform_interface: dependency: transitive description: @@ -116,54 +49,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - bcrypt: - dependency: "direct main" - description: - name: bcrypt - sha256: "6073a700cbbc59f1d4ab27cd532755e3de5e676c4941f535f351374df849270b" - url: "https://pub.dev" - source: hosted - version: "1.2.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" characters: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.3.0" clock: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.dev" source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" + version: "1.1.1" collection: dependency: transitive description: name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.19.1" + version: "1.18.0" connectivity_plus: dependency: "direct main" description: @@ -184,18 +101,18 @@ packages: dependency: transitive description: name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.5+2" + version: "0.3.4+2" crypto: dependency: "direct main" description: name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.3" csslib: dependency: transitive description: @@ -224,26 +141,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c" + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 url: "https://pub.dev" source: hosted - version: "12.3.0" + version: "10.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f - url: "https://pub.dev" - source: hosted - version: "7.0.3" - docman: - dependency: "direct main" - description: - name: docman - sha256: ae25239f40a01617afc3180b55e4909ec383a0777b06a4fd487d53e9916e3e49 + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "7.0.2" dynamic_color: dependency: "direct main" description: @@ -272,26 +181,26 @@ packages: dependency: "direct main" description: name: equations - sha256: "10b744996f9e3440ef66c7facb2adc4015939b8f9d15aebc6d7c3855a72c818c" + sha256: ae30e977d601e19aa1fc3409736c5eac01559d1d653a4c30141fbc4e86aa605c url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "5.0.2" fake_async: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.1" ffi: dependency: transitive description: name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.3" file: dependency: transitive description: @@ -304,10 +213,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" + sha256: "2ca051989f69d1b2ca012b2cf3ccf78c70d40144f0861ff2c063493f7c8c3d45" url: "https://pub.dev" source: hosted - version: "10.3.10" + version: "8.0.5" fixnum: dependency: transitive description: @@ -320,18 +229,18 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: a0979dd61f21b634717b98eb4ceaed2bfe009fe020ce8597aaf164b9eeb57aaa + sha256: "5c846437069fb7afdd7ade6bf37e628a71d2ab0787095ddcb1253bf9345d5f3a" url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.4.1" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: a3183753bbcfc3af106224bff3ab3e1844b73f58062136b7499919f49f3667e7 + sha256: "4cee2f1d07259f77e8b36f4ec5f35499d19e74e17c7dce5b819554914082bc01" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "1.5.0" flutter: dependency: "direct main" description: flutter @@ -397,10 +306,10 @@ packages: dependency: "direct main" description: name: flutter_foreground_task - sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" + sha256: "206017ee1bf864f34b8d7bce664a172717caa21af8da23f55866470dfe316644" url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "8.17.0" flutter_keyboard_visibility: dependency: transitive description: @@ -453,63 +362,47 @@ packages: dependency: "direct main" description: name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "4.0.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: "0d9035862236fe38250fe1644d7ed3b8254e34a21b2c837c9f539fbb3bba5ef1" + sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1 url: "https://pub.dev" source: hosted - version: "21.0.0" + version: "17.0.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: e0f25e243c6c44c825bbbc6b2b2e76f7d9222362adcfe9fd780bf01923c840bd + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "4.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: e7db3d5b49c2b7ecc68deba4aaaa67a348f92ee0fef34c8e4b4459dbef0d7307 + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" url: "https://pub.dev" source: hosted - version: "11.0.0" - flutter_local_notifications_windows: - dependency: transitive - description: - name: flutter_local_notifications_windows - sha256: "3a2654ba104fbb52c618ebed9def24ef270228470718c43b3a6afcd5c81bef0c" - url: "https://pub.dev" - source: hosted - version: "3.0.0" + version: "7.2.0" flutter_localizations: dependency: transitive description: flutter source: sdk version: "0.0.0" - flutter_markdown_plus: - dependency: "direct main" - description: - name: flutter_markdown_plus - sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de" - url: "https://pub.dev" - source: hosted - version: "1.0.7" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" url: "https://pub.dev" source: hosted - version: "2.0.33" + version: "2.0.19" flutter_test: dependency: "direct dev" description: flutter @@ -532,42 +425,18 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "144ddd74d49c865eba47abe31cbc746c7b311c82d6c32e571fd73c4264b740e2" + sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "8.2.8" fraction: dependency: transitive description: name: fraction - sha256: dd487c01c0bfdcccc44d15250b24b4bee94b6981e01e8c41007a1b02f395ea01 + sha256: "09e9504c9177bbd77df56e5d147abfbb3b43360e64bf61510059c14d6a82d524" url: "https://pub.dev" source: hosted - version: "5.0.5" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - gtk: - dependency: transitive - description: - name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.dev" - source: hosted - version: "2.1.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" + version: "5.0.2" hsluv: dependency: "direct main" description: @@ -588,26 +457,26 @@ packages: dependency: "direct main" description: name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.6.0" + version: "1.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.0.2" intl: dependency: transitive description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.18.1" keyboard_detection: dependency: "direct main" description: @@ -620,42 +489,34 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.0" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "2.0.1" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "2.0.1" lints: dependency: transitive description: name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "4.0.0" markdown: dependency: "direct main" description: @@ -668,42 +529,34 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.16+1" material_color_utilities: dependency: "direct main" description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.11.0" mime: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "0.17.4" + version: "1.0.6" nested: dependency: transitive description: @@ -720,46 +573,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" - objective_c: + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + package_info_plus_platform_interface: dependency: transitive description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "2.0.1" path: dependency: "direct main" description: name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.9.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.22" + version: "2.2.4" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -788,18 +649,18 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" url: "https://pub.dev" source: hosted - version: "12.0.1" + version: "11.3.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" url: "https://pub.dev" source: hosted - version: "13.0.1" + version: "12.0.13" permission_handler_apple: dependency: transitive description: @@ -820,10 +681,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.2.3" permission_handler_windows: dependency: transitive description: @@ -836,10 +697,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "5.4.0" platform: dependency: transitive description: @@ -868,10 +729,10 @@ packages: dependency: transitive description: name: pointer_interceptor_ios - sha256: "03c5fa5896080963ab4917eeffda8d28c90f22863a496fb5ba13bc10943e40e4" + sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 url: "https://pub.dev" source: hosted - version: "0.10.1+1" + version: "0.10.1" pointer_interceptor_platform_interface: dependency: transitive description: @@ -884,10 +745,10 @@ packages: dependency: transitive description: name: pointer_interceptor_web - sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a" + sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044" url: "https://pub.dev" source: hosted - version: "0.10.3" + version: "0.10.2+1" provider: dependency: "direct main" description: @@ -896,54 +757,46 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" share_plus: dependency: "direct main" description: name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "12.0.1" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "3.4.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" url: "https://pub.dev" source: hosted - version: "2.4.20" + version: "2.2.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: @@ -964,10 +817,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.1" shared_preferences_windows: dependency: transitive description: @@ -976,132 +829,99 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" - shizuku_apk_installer: - dependency: "direct main" - description: - path: "." - ref: master - resolved-ref: c4349ceb03ae7293987cc0290ef06761e62c082e - url: "https://github.com/wilver06w/shizuku_apk_installer" - source: git - version: "0.0.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" source_span: dependency: transitive description: name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.10.2" + version: "1.10.0" sqflite: dependency: "direct main" description: name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d url: "https://pub.dev" source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 - url: "https://pub.dev" - source: hosted - version: "2.4.2+2" + version: "2.3.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.4" stack_trace: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.6.1" timezone: dependency: transitive description: name: timezone - sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b" + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.9.4" typed_data: dependency: transitive description: name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.2" upower: dependency: transitive description: @@ -1114,42 +934,42 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" url: "https://pub.dev" source: hosted - version: "6.3.28" + version: "6.3.2" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1162,18 +982,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.3" uuid: dependency: transitive description: @@ -1186,42 +1006,42 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "13.0.0" web: dependency: transitive description: name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "0.5.1" win32: dependency: transitive description: name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.15.0" + version: "5.5.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "1.1.3" xdg_directories: dependency: transitive description: @@ -1234,18 +1054,10 @@ packages: dependency: transitive description: name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "6.3.0" sdks: - dart: ">=3.10.4 <4.0.0" - flutter: ">=3.38.4" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index ed481dc8a..30b76e11d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,8 +19,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 26.3.0+26020419 environment: - sdk: ^3.10.0 - flutter: ">=3.38.0" + sdk: ^3.3.0 + flutter: ">=3.19.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -35,66 +35,46 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 #Not needed for now, since Updatium doens't support iOS yet. - path_provider: ^2.1.5 + path_provider: ^2.0.15 flutter_fgbg: ^0.7.1 - flutter_local_notifications: 21.0.0 + flutter_local_notifications: 17.0.0 provider: ^6.1.5+1 - http: ^1.6.0 + http: ^1.1.0 dynamic_color: ^1.8.1 - material_color_utilities: ^0.13.0 + material_color_utilities: ^0.8.0 html: ^0.15.6 - shared_preferences: ^2.5.4 - url_launcher: ^6.3.2 + shared_preferences: ^2.2.0 + url_launcher: ^6.2.0 path: ^1.9.0 - permission_handler: ^12.0.1 - fluttertoast: ^9.0.0 - crypto: ^3.0.6 - device_info_plus: ^12.3.0 - file_picker: ^10.3.10 - animations: ^2.1.1 - android_package_installer: - git: - url: https://github.com/playbott/android_package_installer - ref: main - android_package_manager: # TODO: Replace with another alternative - git: - url: https://github.com/omeritzics/android_package_manager - ref: master - share_plus: ^12.0.1 - sqflite: ^2.4.2 + permission_handler: ^11.0.0 + fluttertoast: ^8.2.0 + crypto: ^3.0.3 + device_info_plus: ^10.0.0 + file_picker: ^8.0.0 + animations: ^2.0.4 + share_plus: ^7.0.0 + sqflite: ^2.3.0 easy_localization: ^3.0.8 android_intent_plus: ^6.0.0 flutter_archive: ^6.0.4 hsluv: ^1.1.3 connectivity_plus: ^7.0.0 - docman: ^1.2.0 - bcrypt: ^1.2.0 - app_links: ^7.0.0 - background_fetch: ^1.5.0 - equations: ^6.0.0 - flex_color_picker: ^3.8.0 - android_system_font: - git: - url: https://github.com/re7gog/android_system_font - ref: master - shizuku_apk_installer: - git: - url: https://github.com/wilver06w/shizuku_apk_installer - ref: master + equations: ^5.0.0 + flex_color_picker: ^3.4.0 markdown: ^7.3.0 flutter_typeahead: ^5.2.0 - battery_plus: ^7.0.0 + battery_plus: ^6.0.0 flutter_charset_detector: ^5.0.0 keyboard_detection: ^0.8.1 + package_info_plus: ^4.0.0 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^6.0.0 - flutter_foreground_task: ^9.2.0 - flutter_markdown_plus: ^1.0.7 + flutter_lints: ^4.0.0 + flutter_foreground_task: ^8.0.0 flutter_launcher_icons: android: "ic_launcher" image_path: "assets/graphics/icon.png" From 73b615bd9b930239f7b20c35bfbe5afb18ef7921 Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Wed, 11 Mar 2026 23:45:22 +0200 Subject: [PATCH 29/46] maybe this one --- pubspec.lock | 18 +----------------- pubspec.yaml | 3 --- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a5ea0a68b..4904e56a5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -358,14 +358,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - flutter_lints: - dependency: "direct main" - description: - name: flutter_lints - sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" - url: "https://pub.dev" - source: hosted - version: "4.0.0" flutter_local_notifications: dependency: "direct main" description: @@ -404,7 +396,7 @@ packages: source: hosted version: "2.0.19" flutter_test: - dependency: "direct dev" + dependency: transitive description: flutter source: sdk version: "0.0.0" @@ -509,14 +501,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - lints: - dependency: transitive - description: - name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" - url: "https://pub.dev" - source: hosted - version: "4.0.0" markdown: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 30b76e11d..d60e15e4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,6 @@ dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^4.0.0 flutter_foreground_task: ^8.0.0 flutter_launcher_icons: android: "ic_launcher" @@ -132,5 +131,3 @@ flutter: - asset: assets/fonts/Inter-Variable.ttf dev_dependencies: - flutter_test: - sdk: flutter From c28bc7cc5dda109ae6609dfad04d01cfaa6074fb Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:09:39 +0200 Subject: [PATCH 30/46] Update lib/providers/apps_provider.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/providers/apps_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 2b781c07e..4817cc065 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -1032,7 +1032,7 @@ class AppsProvider with ChangeNotifier { } catch (_) { // Best-effort cleanup; preserve the security failure below. } - throw UpdatiumError(tr('Security scan detected malware. Installation blocked for safety.')); + throw UpdatiumError(tr('securityScanBlocked')); } logs.add( From 285d6e15466bbd939f852a52e601b8f62c2acb12 Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 15 Mar 2026 14:22:54 +0200 Subject: [PATCH 31/46] Fixes --- .github/workflows/translations.yml | 582 ------------------ android/AndroidManifest.xml | 4 - .../omeritzics/updatium/MainActivity.kt | 98 ++- assets/translations/ar.json | 31 + assets/translations/en.json | 4 +- assets/translations/es.json | 31 + assets/translations/it.json | 31 + assets/translations/ja.json | 31 + assets/translations/pt.json | 31 + assets/translations/ru.json | 31 + assets/translations/zh.json | 31 + lib/pages/settings.dart | 2 +- lib/providers/apps_provider.dart | 53 +- lib/security/security_settings_provider.dart | 25 +- lib/security/yara_scanner.dart | 164 ++++- 15 files changed, 518 insertions(+), 631 deletions(-) delete mode 100644 .github/workflows/translations.yml delete mode 100644 android/AndroidManifest.xml diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml deleted file mode 100644 index db9fb96fd..000000000 --- a/.github/workflows/translations.yml +++ /dev/null @@ -1,582 +0,0 @@ -name: Translation Management - -on: - push: - branches: [ main, develop ] - paths: - - 'assets/translations/en.json' - - 'lib/**/*.dart' - pull_request: - branches: [ main, develop ] - paths: - - 'assets/translations/en.json' - - 'lib/**/*.dart' - workflow_dispatch: - inputs: - auto_translate: - description: 'Auto-translate missing strings using LibreTranslate' - required: false - default: 'false' - type: boolean - remove_unused: - description: 'Remove unused translation strings' - required: false - default: 'false' - type: boolean - -# Set permissions for the workflow -permissions: - contents: write # Allow reading and writing to repository contents - pull-requests: write # Allow creating and updating pull requests - checks: read # Allow reading check statuses (for PR creation) - -jobs: - detect-unused-strings: - runs-on: ubuntu-latest - name: Detect Unused Translation Strings - if: github.event.inputs.remove_unused == 'true' - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - - - name: Install dependencies - run: | - cd assets/translations - npm install - - - name: Extract used translation keys from Dart files - id: extract - run: | - echo "Extracting translation keys from Dart files..." - - # Create a script to extract translation keys from Dart files - cat > extract_keys.js << 'EOF' - const fs = require('fs'); - const path = require('path'); - - // Function to extract translation keys from a single file - function extractKeysFromFile(filePath) { - const content = fs.readFileSync(filePath, 'utf8'); - const keys = new Set(); - - // Match patterns like: - // - tr('key') - // - tr('key', args: [...]) - // - AppLocalizations.of(context)!.key - // - AppLocalizations.of(context)!.key(args: [...]) - // - tr('x', args: [plural('key', ...)]) - // - AppLocalizations.of(context)!.x(plural('key', ...)) - - const patterns = [ - /tr\(\s*['"`]([^'"`]+)['"`]/g, - /tr\(\s*['"`]([^'"`]+)['"`]\s*,/g, - /AppLocalizations\.of\(context\)\!\.([a-zA-Z_][a-zA-Z0-9_]*)/g, - /plural\(\s*['"`]([^'"`]+)['"`]/g, - /AppLocalizations\.of\(context\)\!\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, - ]; - - patterns.forEach(pattern => { - let match; - while ((match = pattern.exec(content)) !== null) { - const key = match[1]; - if (key && !key.includes(' ') && key.length > 0) { - keys.add(key); - } - } - }); - - return Array.from(keys); - } - - // Function to recursively find all Dart files - function findDartFiles(dir) { - const files = []; - - function traverse(currentDir) { - const items = fs.readdirSync(currentDir); - - for (const item of items) { - const fullPath = path.join(currentDir, item); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory() && !item.startsWith('.') && item !== 'build') { - traverse(fullPath); - } else if (stat.isFile() && item.endsWith('.dart')) { - files.push(fullPath); - } - } - } - - traverse(dir); - return files; - } - - // Main execution - const libDir = path.join(process.cwd(), 'lib'); - const dartFiles = findDartFiles(libDir); - - console.log(`Found ${dartFiles.length} Dart files`); - - const allKeys = new Set(); - - dartFiles.forEach(file => { - const keys = extractKeysFromFile(file); - keys.forEach(key => allKeys.add(key)); - }); - - const sortedKeys = Array.from(allKeys).sort(); - - // Save to file - fs.writeFileSync('used_keys.txt', sortedKeys.join('\n')); - - console.log(`Extracted ${sortedKeys.length} unique translation keys`); - console.log('Keys saved to used_keys.txt'); - - // Output for GitHub Actions - console.log(`used_keys=${sortedKeys.join(',')}`); - console.log(`used_keys_count=${sortedKeys.length}`); - EOF - - # Run the extraction script - cd assets/translations - node ../extract_keys.js - - # Capture output for GitHub Actions - used_keys=$(cat used_keys.txt | tr '\n' ',') - used_keys_count=$(wc -l < used_keys.txt) - - echo "used_keys=$used_keys" >> $GITHUB_OUTPUT - echo "used_keys_count=$used_keys_count" >> $GITHUB_OUTPUT - - echo "🔍 Found $used_keys_count translation keys used in code" - - - name: Detect unused translation keys - id: detect_unused - run: | - cd assets/translations - - echo "Detecting unused translation keys..." - - template_file="en.json" - template_keys=$(jq -r 'keys[]' "$template_file" | sort) - - # Get used keys from previous step - IFS=',' read -ra USED_KEYS <<< "${{ steps.extract.outputs.used_keys }}" - - unused_keys=0 - unused_list="" - - echo "Checking each template key against used keys..." - - for key in $template_keys; do - if [[ ! " ${USED_KEYS[@]} " =~ " ${key} " ]]; then - echo "❌ Unused key: $key" - unused_keys=$((unused_keys + 1)) - unused_list="$unused_list $key" - else - echo "✅ Used key: $key" - fi - done - - echo "unused_keys=$unused_keys" >> $GITHUB_OUTPUT - echo "unused_list=$unused_list" >> $GITHUB_OUTPUT - - if [ $unused_keys -gt 0 ]; then - echo "⚠️ Found $unused_keys unused translation keys" - echo "Unused keys:$unused_list" - else - echo "✅ All translation keys are in use" - fi - - - name: Remove unused translation keys - if: steps.detect_unused.outputs.unused_keys > 0 - run: | - cd assets/translations - - echo "🗑️ Removing unused translation keys..." - - template_file="en.json" - - # Create backup - cp "$template_file" "${template_file}.backup" - - # Get unused keys - IFS=' ' read -ra UNUSED_KEYS <<< "${{ steps.detect_unused.outputs.unused_list }}" - - echo "Removing ${#UNUSED_KEYS[@]} unused keys from all translation files..." - - for file in *.json; do - if [ "$file" != "package.json" ] && [ "$file" != "used_keys.txt" ]; then - echo "Processing $file..." - - # Create a new JSON without unused keys - jq --arg keys "$(printf '%s\n' "${UNUSED_KEYS[@]}" | jq -R . | jq -s .)" ' - reduce . as $in ($ARGS.positional[]; select($in | has($in)) | del($in[$in])) - ' "$file" > "${file}.tmp" - - # Replace original file - mv "${file}.tmp" "$file" - - echo "✅ Updated $file" - fi - done - - echo "🗑️ Unused keys have been removed" - echo "📁 Backups created with .backup extension" - - - name: Create Pull Request for unused keys removal - if: steps.detect_unused.outputs.unused_keys > 0 - uses: peter-evans/create-pull-request@v8 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "🗑️ Remove unused translation keys" - title: "🗑️ Remove Unused Translation Keys" - body: | - ## 🗑️ Unused Translation Keys Removal - - This PR automatically removes unused translation keys that were found in the codebase. - - ### 📊 Summary - - **Unused keys found**: ${{ steps.detect_unused.outputs.unused_keys }} - - **Keys in use**: ${{ steps.extract.outputs.used_keys_count }} - - **Files updated**: All translation files - - ### 🔧 What was done - - ✅ Analyzed all Dart files for translation key usage - - ✅ Identified ${{ steps.detect_unused.outputs.unused_keys }} unused keys - - ✅ Removed unused keys from all translation files - - ✅ Created backups of original files - - ### 🗑️ Removed Keys - ``` - ${{ steps.detect_unused.outputs.unused_list }} - ``` - - ### 📝 Next Steps - 1. Review the removed keys to ensure they're truly unused - 2. Check if any keys are used dynamically (e.g., via string concatenation) - 3. Test the app to ensure no functionality is broken - 4. Remove backup files if everything works correctly - - ### ⚠️ Important - If any keys were removed in error, you can: - - Restore from the `.backup` files - - Manually add back any needed keys - - ### 🤖 Auto-generated - This PR was automatically created by the Translation Management workflow. - - branch: remove-unused-translations - delete-branch: true - labels: | - translations - cleanup - automated - - detect-missing-translations: - runs-on: ubuntu-latest - name: Detect Missing Translations - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 # Fetch full history for proper PR creation - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - - - name: Install dependencies - run: | - cd assets/translations - npm install - - - name: Detect missing translation keys - id: detect - run: | - cd assets/translations - - # Get all translation files - template_file="en.json" - other_files=$(ls *.json | grep -v "$template_file" | grep -v "package") - - missing_keys=0 - missing_files="" - - echo "Checking for missing translation keys..." - - for file in $other_files; do - echo "Checking $file..." - - # Get keys from template and current file - template_keys=$(jq -r 'keys[]' "$template_file" | sort) - current_keys=$(jq -r 'keys[]' "$file" | sort) - - # Find missing keys - missing=$(comm -23 <(echo "$template_keys") <(echo "$current_keys")) - - if [ -n "$missing" ]; then - echo "❌ Missing keys in $file:" - echo "$missing" - missing_keys=$((missing_keys + $(echo "$missing" | wc -l))) - missing_files="$missing_files $file" - else - echo "✅ All keys present in $file" - fi - done - - echo "missing_keys=$missing_keys" >> $GITHUB_OUTPUT - echo "missing_files=$missing_files" >> $GITHUB_OUTPUT - - if [ $missing_keys -gt 0 ]; then - echo "⚠️ Found $missing_keys missing translation keys" - exit 1 - else - echo "✅ All translation files are up to date" - fi - - - name: Add missing translation keys - if: steps.detect.outputs.missing_keys > 0 - run: | - cd assets/translations - - echo "Adding missing translation keys..." - - # Run the standardization script to add missing keys - node standardize.mjs - - echo "✅ Missing keys have been added with English fallbacks" - - - name: Auto-translate missing strings (optional) - if: steps.detect.outputs.missing_keys > 0 && github.event.inputs.auto_translate == 'true' - env: - LIBRETRANSLATE_API_KEY: ${{ secrets.LIBRETRANSLATE_API_KEY }} - run: | - cd assets/translations - - if [ -z "$LIBRETRANSLATE_API_KEY" ]; then - echo "⚠️ LIBRETRANSLATE_API_KEY not found, skipping auto-translation" - exit 0 - fi - - echo "Auto-translating missing strings..." - - # Modified script to translate only missing keys - node -e " - const fs = require('fs'); - const translate = require('translate'); - - translate.engine = 'libre'; - translate.key = process.env.LIBRETRANSLATE_API_KEY; - translate.from = 'en'; - translate.url = 'https://libretranslate.de/translate'; - - const templateFile = 'en.json'; - const templateTranslation = JSON.parse(fs.readFileSync(templateFile).toString()); - const otherFiles = fs.readdirSync('.').filter(f => f.endsWith('.json') && f !== templateFile && !f.startsWith('package')); - - const neverAutoTranslate = { - steamMobile: ['*'], - steamChat: ['*'], - root: ['*'], - updatiumExportHyphenatedLowercase: ['*'], - theme: ['de'], - appId: ['de'], - app: ['de'], - apps: ['de', 'gl'], - placeholder: ['pl'], - importExport: ['fr'], - url: ['fr', 'ca', 'de', 'gl', 'pt', 'pt-BR'], - vivoAppStore: ['*'], - coolApk: ['*'], - updatiumImport: ['nl'], - appLogs: ['nl'], - apk: ['vi', 'ar', 'ca', 'de', 'es', 'gl'], - minute: ['fr'], - pseudoVersion: ['da'], - tencentAppStore: ['*'] - }; - - const shouldSkipAutoTranslate = (key, lang) => { - if (neverAutoTranslate[key] && (neverAutoTranslate[key].includes('*') || neverAutoTranslate[key].includes(lang))) { - return true; - } - return false; - }; - - async function translateMissing() { - for (const file of otherFiles) { - const lang = file.replace('.json', ''); - const translation = JSON.parse(fs.readFileSync(file).toString()); - let modified = false; - - for (const [key, value] of Object.entries(templateTranslation)) { - if (!translation[key] || translation[key] === value) { - if (shouldSkipAutoTranslate(key, lang)) { - console.log(\`Skipping auto-translation of '\${key}' for \${lang}\`); - continue; - } - - try { - console.log(\`Translating '\${key}' to \${lang}\`); - const translated = await translate(value, lang.slice(0, 2)); - translation[key] = translated; - modified = true; - } catch (error) { - console.log(\`Failed to translate '\${key}' to \${lang}: \${error.message}\`); - } - } - } - - if (modified) { - fs.writeFileSync(file, JSON.stringify(translation, null, 4) + '\\n'); - console.log(\`Updated \${file}\`); - } - } - } - - translateMissing().catch(console.error); - " - - echo "✅ Auto-translation completed" - - - name: Create Pull Request - if: steps.detect.outputs.missing_keys > 0 - uses: peter-evans/create-pull-request@v8 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "🌐 Add missing translation keys" - title: "🌐 Add Missing Translation Keys" - body: | - ## 🌐 Missing Translation Keys Detected - - This PR automatically adds missing translation keys that were found in the codebase. - - ### 📊 Summary - - **Missing keys found**: ${{ steps.detect.outputs.missing_keys }} - - **Files updated**: ${{ steps.detect.outputs.missing_files }} - - ### 🔧 What was done - - ✅ Added missing keys with English fallback translations - ${{ github.event.inputs.auto_translate == 'true' && '- ✅ Auto-translated missing strings using LibreTranslate' || '' }} - - ### 📝 Next Steps - 1. Review the added translations - 2. Replace auto-translations with proper human translations if needed - 3. Update any keys that need context-specific translations - - ### 🤖 Auto-generated - This PR was automatically created by the Translation Management workflow. - - branch: auto-add-translations - delete-branch: true - labels: | - translations - automated - - - name: Comment on PR (if auto-translated) - if: steps.detect.outputs.missing_keys > 0 && github.event.inputs.auto_translate == 'true' - uses: actions/github-script@v8 - with: - script: | - const comment = `## 🤖 Auto-Translation Notice - - This PR includes automatically translated strings using LibreTranslate. - - ⚠️ **Important**: Please review all auto-translated strings carefully: - - Auto-translations may not be perfect - - Some strings may need cultural adaptation - - Technical terms might need manual correction - - ### 🔍 Review Checklist - - [ ] Check all new translations for accuracy - - [ ] Verify cultural appropriateness - - [ ] Ensure technical terms are correct - - [ ] Test the app with new translations - - Thank you for helping maintain translation quality! 🌍`; - - // This will comment on the PR that was just created - console.log('Translation review comment would be added to the PR'); - - validate-translations: - runs-on: ubuntu-latest - name: Validate Translation Format - needs: detect-missing-translations - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Validate JSON format - run: | - cd assets/translations - - echo "Validating JSON format for all translation files..." - - for file in *.json; do - if [ "$file" != "package.json" ]; then - echo "Validating $file..." - if jq empty "$file" 2>/dev/null; then - echo "✅ $file is valid JSON" - else - echo "❌ $file has invalid JSON format" - jq . "$file" 2>&1 | head -10 - exit 1 - fi - fi - done - - - name: Check for duplicate keys - run: | - cd assets/translations - - echo "Checking for duplicate keys..." - - for file in *.json; do - if [ "$file" != "package.json" ]; then - duplicates=$(jq -r 'keys[]' "$file" | sort | uniq -d) - if [ -n "$duplicates" ]; then - echo "❌ Duplicate keys found in $file:" - echo "$duplicates" - exit 1 - else - echo "✅ No duplicate keys in $file" - fi - fi - done - - - name: Check key consistency - run: | - cd assets/translations - - echo "Checking key consistency across all files..." - - template_file="en.json" - template_keys=$(jq -r 'keys[]' "$template_file" | sort) - - for file in *.json; do - if [ "$file" != "package.json" ] && [ "$file" != "$template_file" ]; then - current_keys=$(jq -r 'keys[]' "$file" | sort) - - extra_keys=$(comm -13 <(echo "$template_keys") <(echo "$current_keys")) - if [ -n "$extra_keys" ]; then - echo "⚠️ Extra keys in $file (not in template):" - echo "$extra_keys" - fi - fi - done - - echo "✅ Key consistency check completed" diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml deleted file mode 100644 index da7535cf2..000000000 --- a/android/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt b/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt index f10d62308..843c4c6f6 100644 --- a/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt +++ b/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt @@ -1,5 +1,101 @@ package io.github.omeritzics.updatium +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Build import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel -class MainActivity : FlutterActivity() +class MainActivity : FlutterActivity() { + private val CHANNEL = "updatium/package_manager" + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "getInstalledApps" -> { + try { + val installedApps = getInstalledAppsList() + result.success(installedApps) + } catch (e: Exception) { + result.error("ERROR", "Failed to get installed apps", e.message) + } + } + "getAppInfo" -> { + try { + val packageName = call.argument("packageName") + if (packageName != null) { + val appInfo = getAppInfo(packageName) + result.success(appInfo) + } else { + result.error("INVALID_ARGUMENT", "Package name is required", null) + } + } catch (e: Exception) { + result.error("ERROR", "Failed to get app info", e.message) + } + } + else -> { + result.notImplemented() + } + } + } + } + + private fun getInstalledAppsList(): List> { + val packageManager = packageManager + val installedPackages = packageManager.getInstalledPackages(PackageManager.GET_META_DATA) + val appsList = mutableListOf>() + + for (packageInfo in installedPackages) { + try { + val applicationInfo = packageInfo.applicationInfo + // Only include user apps (not system apps) and apps that have a launch intent + if (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0 && + packageManager.getLaunchIntentForPackage(packageInfo.packageName) != null) { + + val appMap = mapOf( + "appName" to packageManager.getApplicationLabel(applicationInfo).toString(), + "packageName" to packageInfo.packageName, + "version" to packageInfo.versionName ?: "", + "buildNumber" to if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode.toString() + } else { + @Suppress("DEPRECATION") + packageInfo.versionCode.toString() + } + ) + appsList.add(appMap) + } + } catch (e: Exception) { + // Skip apps that can't be accessed + } + } + + return appsList + } + + private fun getAppInfo(packageName: String): Map? { + return try { + val packageManager = packageManager + val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA) + val applicationInfo = packageInfo.applicationInfo + + mapOf( + "appName" to packageManager.getApplicationLabel(applicationInfo).toString(), + "packageName" to packageInfo.packageName, + "version" to packageInfo.versionName ?: "", + "buildNumber" to if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode.toString() + } else { + @Suppress("DEPRECATION") + packageInfo.versionCode.toString() + } + ) + } catch (e: PackageManager.NameNotFoundException) { + null + } catch (e: Exception) { + null + } + } +} diff --git a/assets/translations/ar.json b/assets/translations/ar.json index e51b97599..78eaba6f2 100644 --- a/assets/translations/ar.json +++ b/assets/translations/ar.json @@ -28,6 +28,37 @@ "githubStarredRepos": "مستودعات GitHub المفضلة", "uname": "اسم المستخدم", "wrongArgNum": "عدد وسائط غير صحيح", + "yaraMalwareScanner": "فحص البرامج الضارة التلقائي (قواعد نمط YARA)", + "yaraScannerDescription": "كشف أساسي للبرامج الضارة باستخدام قواعد مطابقة السلاسل نمط YARA للحماية من التهديدات المعروفة. هذا التطبيق يوفر قدرات أساسية لمطابقة الأنماط. تقع مسؤولية الاستخدام الآمن للتطبيق على المستخدم في النهاية.", + "securitySettings": "إعدادات الأمان", + "enableAutoScan": "تفعيل الفحص التلقائي", + "enableAutoScanDescription": "فحص ملفات APK التي تم تنزيلها تلقائيًا بحثًا عن برامج ضارة قبل التثبيت", + "enableAutoUpdate": "تفعيل التحديث التلقائي", + "enableAutoUpdateDescription": "تحديث قاعدة بيانات تعريفات البرامج الضارة تلقائيًا", + "updateInterval": "الفاصل الزمني للتحديث", + "updateIntervalDescription": "مدى التكرار للتحقق من تعريفات البرامج الضارة الجديدة", + "hours": "ساعات", + "threatLevelFilter": "مرشح مستوى التهديد", + "threatLevelFilterDescription": "الحد الأدنى لمستوى التهديد لتشغيل التنبيهات", + "level1": "المستوى 1", + "lowThreat": "تهديد منخفض", + "level2": "المستوى 2", + "mediumThreat": "تهديد متوسط", + "level3": "المستوى 3", + "highThreat": "تهديد عالٍ", + "quarantineSettings": "إعدادات الحجر الصحي", + "quarantineInfected": "حجر الملفات المصابة", + "quarantineInfectedDescription": "نقل البرامج الضارة المكتشفة إلى الحجر الصحي للسلامة", + "viewQuarantine": "عرض الحجر الصحي", + "viewQuarantineDescription": "إدارة الملفات المحجورة", + "databaseInformation": "معلومات قاعدة البيانات", + "lastUpdate": "آخر تحديث", + "rulesVersion": "إصدار القواعد", + "updateNow": "تحديث الآن", + "updating": "جاري التحديث...", + "rulesUpdatedSuccessfully": "تم تحديث تعريفات البرامج الضارة بنجاح", + "rulesUpdateFailed": "فشل تحديث تعريفات البرامج الضارة", + "quarantineViewComingSoon": "عرض الحجر الصحي قريبًا", "xIsTrackOnly": "{} للتعقب فقط", "source": "المصدر", "app": "التطبيق", diff --git a/assets/translations/en.json b/assets/translations/en.json index 07b043045..7b79c8c55 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -28,8 +28,8 @@ "githubStarredRepos": "GitHub starred repositories", "uname": "Username", "wrongArgNum": "Wrong number of arguments provided", - "yaraMalwareScanner": "Auto Malware Scanning (Powered by YARA)", - "yaraScannerDescription": "Industry-standard malware detection using YARA rules for comprehensive protection against viruses, trojans, and other threats. While no scanner can guarantee 100% detection, this provides strong protection against known malware. The responsibility for safe app usage ultimately is on the user.", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", "securitySettings": "Security Settings", "enableAutoScan": "Enable Auto-Scan", "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", diff --git a/assets/translations/es.json b/assets/translations/es.json index b7d080680..421c582b3 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -28,6 +28,37 @@ "githubStarredRepos": "repositorios favoritos en GitHub", "uname": "Nombre de usuario", "wrongArgNum": "Número de argumentos provistos inválido", + "yaraMalwareScanner": "Escaneo Automático de Malware (Reglos Estilo YARA)", + "yaraScannerDescription": "Detección básica de malware usando reglas de coincidencia de cadenas estilo YARA para protección contra amenazas conocidas. Esta implementación proporciona capacidades fundamentales de coincidencia de patrones. La responsabilidad del uso seguro de la aplicación recae finalmente en el usuario.", + "securitySettings": "Configuración de Seguridad", + "enableAutoScan": "Activar Escaneo Automático", + "enableAutoScanDescription": "Escanear automáticamente los APK descargados en busca de malware antes de la instalación", + "enableAutoUpdate": "Activar Actualización Automática", + "enableAutoUpdateDescription": "Actualizar automáticamente la base de datos de definiciones de malware", + "updateInterval": "Intervalo de Actualización", + "updateIntervalDescription": "Con qué frecuencia buscar nuevas definiciones de malware", + "hours": "horas", + "threatLevelFilter": "Filtro de Nivel de Amenaza", + "threatLevelFilterDescription": "Nivel mínimo de amenaza para activar alertas", + "level1": "Nivel 1", + "lowThreat": "Amenaza Baja", + "level2": "Nivel 2", + "mediumThreat": "Amenaza Media", + "level3": "Nivel 3", + "highThreat": "Amenaza Alta", + "quarantineSettings": "Configuración de Cuarentena", + "quarantineInfected": "Poner en Cuarentena Archivos Infectados", + "quarantineInfectedDescription": "Mover el malware detectado a cuarentena para mayor seguridad", + "viewQuarantine": "Ver Cuarentena", + "viewQuarantineDescription": "Gestionar archivos en cuarentena", + "databaseInformation": "Información de la Base de Datos", + "lastUpdate": "Última Actualización", + "rulesVersion": "Versión de Reglas", + "updateNow": "Actualizar Ahora", + "updating": "Actualizando...", + "rulesUpdatedSuccessfully": "Definiciones de malware actualizadas exitosamente", + "rulesUpdateFailed": "Error al actualizar definiciones de malware", + "quarantineViewComingSoon": "Vista de cuarentena próximamente", "xIsTrackOnly": "{} es de 'sólo seguimiento'", "source": "fuente", "app": "Aplicación", diff --git a/assets/translations/it.json b/assets/translations/it.json index bd0da9d8e..3af5114a4 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -28,6 +28,37 @@ "githubStarredRepos": "repository stellati da GitHub", "uname": "Nome utente", "wrongArgNum": "Numero di argomenti forniti errato", + "yaraMalwareScanner": "Scansione Automatica Malware (Regole Stile YARA)", + "yaraScannerDescription": "Rilevamento di malware di base utilizzando regole di corrispondenza delle stringhe in stile YARA per la protezione contro minacce conosciute. Questa implementazione fornisce capacità fondamentali di corrispondenza dei pattern. La responsabilità dell'uso sicuro dell'app spetta infine all'utente.", + "securitySettings": "Impostazioni di Sicurezza", + "enableAutoScan": "Abilita Scansione Automatica", + "enableAutoScanDescription": "Scansiona automaticamente gli APK scaricati alla ricerca di malware prima dell'installazione", + "enableAutoUpdate": "Abilita Aggiornamento Automatico", + "enableAutoUpdateDescription": "Aggiorna automaticamente il database delle definizioni malware", + "updateInterval": "Intervallo di Aggiornamento", + "updateIntervalDescription": "Frequenza con cui cercare nuove definizioni malware", + "hours": "ore", + "threatLevelFilter": "Filtro Livello Minaccia", + "threatLevelFilterDescription": "Livello minimo di minaccia per attivare avvisi", + "level1": "Livello 1", + "lowThreat": "Minaccia Bassa", + "level2": "Livello 2", + "mediumThreat": "Minaccia Media", + "level3": "Livello 3", + "highThreat": "Minaccia Alta", + "quarantineSettings": "Impostazioni Quarantena", + "quarantineInfected": "Metti in Quarantena File Infetti", + "quarantineInfectedDescription": "Sposta il malware rilevato in quarantena per sicurezza", + "viewQuarantine": "Visualizza Quarantena", + "viewQuarantineDescription": "Gestisci file in quarantena", + "databaseInformation": "Informazioni Database", + "lastUpdate": "Ultimo Aggiornamento", + "rulesVersion": "Versione Regole", + "updateNow": "Aggiorna Ora", + "updating": "Aggiornamento...", + "rulesUpdatedSuccessfully": "Definizioni malware aggiornate con successo", + "rulesUpdateFailed": "Fallito aggiornamento definizioni malware", + "quarantineViewComingSoon": "Vista quarantena in arrivo", "xIsTrackOnly": "{} è in modalità Solo-Monitoraggio", "source": "Fonte", "app": "App", diff --git a/assets/translations/ja.json b/assets/translations/ja.json index 3fa5af32d..35d6c658c 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -28,6 +28,37 @@ "githubStarredRepos": "GitHubでスターしたリポジトリ", "uname": "ユーザー名", "wrongArgNum": "引数の数が間違っています", + "yaraMalwareScanner": "自動マルウェアスキャン (YARAスタイルルール)", + "yaraScannerDescription": "既知の脅威から保護するため、YARAスタイルの文字列マッチングルールを使用した基本的なマルウェア検出。この実装は基本的なパターンマッチング機能を提供します。アプリの安全な使用の最終的な責任はユーザーにあります。", + "securitySettings": "セキュリティ設定", + "enableAutoScan": "自動スキャンを有効にする", + "enableAutoScanDescription": "インストール前にダウンロードしたAPKを自動でマルウェアスキャンする", + "enableAutoUpdate": "自動更新を有効にする", + "enableAutoUpdateDescription": "マルウェア定義データベースを自動的に更新する", + "updateInterval": "更新間隔", + "updateIntervalDescription": "新しいマルウェア定義をチェックする頻度", + "hours": "時間", + "threatLevelFilter": "脅威レベルフィルター", + "threatLevelFilterDescription": "アラートをトリガーする最小脅威レベル", + "level1": "レベル1", + "lowThreat": "低脅威", + "level2": "レベル2", + "mediumThreat": "中脅威", + "level3": "レベル3", + "highThreat": "高脅威", + "quarantineSettings": "検疫設定", + "quarantineInfected": "感染ファイルを検疫", + "quarantineInfectedDescription": "検出されたマルウェアを安全のため検疫に移動する", + "viewQuarantine": "検疫を表示", + "viewQuarantineDescription": "検疫されたファイルを管理する", + "databaseInformation": "データベース情報", + "lastUpdate": "最終更新", + "rulesVersion": "ルールバージョン", + "updateNow": "今すぐ更新", + "updating": "更新中...", + "rulesUpdatedSuccessfully": "マルウェア定義が正常に更新されました", + "rulesUpdateFailed": "マルウェア定義の更新に失敗しました", + "quarantineViewComingSoon": "検疫ビューは近日公開", "xIsTrackOnly": "{} は「追跡のみ」です", "source": "ソース", "app": "アプリ", diff --git a/assets/translations/pt.json b/assets/translations/pt.json index 2255133ef..bd57d8487 100644 --- a/assets/translations/pt.json +++ b/assets/translations/pt.json @@ -28,6 +28,37 @@ "githubStarredRepos": "repositórios favoritos no GitHub", "uname": "Nome de usuário", "wrongArgNum": "Número de argumentos errado", + "yaraMalwareScanner": "Escaneamento Automático de Malware (Regras Estilo YARA)", + "yaraScannerDescription": "Detecção básica de malware usando regras de correspondência de strings estilo YARA para proteção contra ameaças conhecidas. Esta implementação fornece capacidades fundamentais de correspondência de padrões. A responsabilidade pelo uso seguro do aplicativo cabe finalmente ao usuário.", + "securitySettings": "Configurações de Segurança", + "enableAutoScan": "Ativar Escaneamento Automático", + "enableAutoScanDescription": "Escanear automaticamente os APKs baixados em busca de malware antes da instalação", + "enableAutoUpdate": "Ativar Atualização Automática", + "enableAutoUpdateDescription": "Atualizar automaticamente o banco de dados de definições de malware", + "updateInterval": "Intervalo de Atualização", + "updateIntervalDescription": "Com que frequência verificar novas definições de malware", + "hours": "horas", + "threatLevelFilter": "Filtro de Nível de Ameaça", + "threatLevelFilterDescription": "Nível mínimo de ameaça para acionar alertas", + "level1": "Nível 1", + "lowThreat": "Ameaça Baixa", + "level2": "Nível 2", + "mediumThreat": "Ameaça Média", + "level3": "Nível 3", + "highThreat": "Ameaça Alta", + "quarantineSettings": "Configurações de Quarentena", + "quarantineInfected": "Colocar Arquivos Infectados em Quarentena", + "quarantineInfectedDescription": "Mover malware detectado para quarentena para segurança", + "viewQuarantine": "Ver Quarentena", + "viewQuarantineDescription": "Gerenciar arquivos em quarentena", + "databaseInformation": "Informações do Banco de Dados", + "lastUpdate": "Última Atualização", + "rulesVersion": "Versão das Regras", + "updateNow": "Atualizar Agora", + "updating": "Atualizando...", + "rulesUpdatedSuccessfully": "Definições de malware atualizadas com sucesso", + "rulesUpdateFailed": "Falha ao atualizar definições de malware", + "quarantineViewComingSoon": "Visualização de quarentena em breve", "xIsTrackOnly": "{} é 'Apenas monitorar'", "source": "Fonte", "app": "Aplicação", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 0db9e3b30..cffed886a 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Избранные репозитории GitHub", "uname": "Имя пользователя", "wrongArgNum": "Неправильное количество предоставленных аргументов", + "yaraMalwareScanner": "Автоматическое сканирование на вредоносное ПО (правила в стиле YARA)", + "yaraScannerDescription": "Базовое обнаружение вредоносного ПО с использованием правил сопоставления строк в стиле YARA для защиты от известных угроз. Эта реализация обеспечивает фундаментальные возможности сопоставления шаблонов. Конечная ответственность за безопасное использование приложения лежит на пользователе.", + "securitySettings": "Настройки безопасности", + "enableAutoScan": "Включить автоматическое сканирование", + "enableAutoScanDescription": "Автоматически сканировать загруженные APK на наличие вредоносного ПО перед установкой", + "enableAutoUpdate": "Включить автоматическое обновление", + "enableAutoUpdateDescription": "Автоматически обновлять базу данных определений вредоносного ПО", + "updateInterval": "Интервал обновления", + "updateIntervalDescription": "Как часто проверять новые определения вредоносного ПО", + "hours": "часов", + "threatLevelFilter": "Фильтр уровня угрозы", + "threatLevelFilterDescription": "Минимальный уровень угрозы для triggering оповещений", + "level1": "Уровень 1", + "lowThreat": "Низкая угроза", + "level2": "Уровень 2", + "mediumThreat": "Средняя угроза", + "level3": "Уровень 3", + "highThreat": "Высокая угроза", + "quarantineSettings": "Настройки карантина", + "quarantineInfected": "Поместить инфицированные файлы в карантин", + "quarantineInfectedDescription": "Переместить обнаруженное вредоносное ПО в карантин для безопасности", + "viewQuarantine": "Просмотр карантина", + "viewQuarantineDescription": "Управление файлами в карантине", + "databaseInformation": "Информация о базе данных", + "lastUpdate": "Последнее обновление", + "rulesVersion": "Версия правил", + "updateNow": "Обновить сейчас", + "updating": "Обновление...", + "rulesUpdatedSuccessfully": "Определения вредоносного ПО успешно обновлены", + "rulesUpdateFailed": "Не удалось обновить определения вредоносного ПО", + "quarantineViewComingSoon": "Просмотр карантина скоро будет доступен", "xIsTrackOnly": "{} только для отслеживания", "source": "Источник", "app": "Приложение", diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 88a013ce5..36154cbd0 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -28,6 +28,37 @@ "githubStarredRepos": "已星标的 GitHub 仓库", "uname": "用户名", "wrongArgNum": "参数数量错误", + "yaraMalwareScanner": "自动恶意软件扫描 (YARA样式规则)", + "yaraScannerDescription": "使用YARA样式字符串匹配规则进行基本恶意软件检测,以保护抵御已知威胁。此实现提供基本的模式匹配功能。安全使用应用程序的最终责任在于用户。", + "securitySettings": "安全设置", + "enableAutoScan": "启用自动扫描", + "enableAutoScanDescription": "在安装前自动扫描下载的APK文件中的恶意软件", + "enableAutoUpdate": "启用自动更新", + "enableAutoUpdateDescription": "自动更新恶意软件定义数据库", + "updateInterval": "更新间隔", + "updateIntervalDescription": "检查新恶意软件定义的频率", + "hours": "小时", + "threatLevelFilter": "威胁级别过滤器", + "threatLevelFilterDescription": "触发警报的最低威胁级别", + "level1": "级别 1", + "lowThreat": "低威胁", + "level2": "级别 2", + "mediumThreat": "中等威胁", + "level3": "级别 3", + "highThreat": "高威胁", + "quarantineSettings": "隔离设置", + "quarantineInfected": "隔离感染文件", + "quarantineInfectedDescription": "将检测到的恶意软件移动到隔离区以确保安全", + "viewQuarantine": "查看隔离区", + "viewQuarantineDescription": "管理隔离文件", + "databaseInformation": "数据库信息", + "lastUpdate": "上次更新", + "rulesVersion": "规则版本", + "updateNow": "立即更新", + "updating": "更新中...", + "rulesUpdatedSuccessfully": "恶意软件定义更新成功", + "rulesUpdateFailed": "更新恶意软件定义失败", + "quarantineViewComingSoon": "隔离区视图即将推出", "xIsTrackOnly": "“{}”为“仅追踪”模式", "source": "来源", "app": "应用", diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 4b6a414b8..90efee531 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -918,7 +918,7 @@ onChanged: _securityProviderInitialized ? (value) async { items: [1, 6, 12, 24, 48, 72].map((hours) { return DropdownMenuItem( value: hours, - child: Text('$hours ${tr('hours')}'), + child: Text('$hours ${hours == 1 ? tr('hour') : tr('hours')}'), ); }).toList(), onChanged: _securityProviderInitialized ? (value) async { diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 4817cc065..c270bd97b 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -40,6 +40,7 @@ import 'package:android_intent_plus/android_intent.dart'; import 'package:flutter_archive/flutter_archive.dart'; import 'package:share_plus/share_plus.dart'; import 'package:updatium/security/security_settings_provider.dart'; +import 'package:package_info_plus/package_info_plus.dart'; class AppInMemory { @@ -498,21 +499,63 @@ Future downloadFile( } Future> getAllInstalledInfo() async { - return []; + try { + // Use platform channel to get all installed apps + const platform = MethodChannel('updatium/package_manager'); + final List installedApps = await platform.invokeMethod('getInstalledApps'); + + List packageInfoList = []; + for (var appData in installedApps) { + try { + // Create PackageInfo objects from the platform data + final packageInfo = PackageInfo( + appName: appData['appName'] ?? '', + packageName: appData['packageName'] ?? '', + version: appData['version'] ?? '', + buildNumber: appData['buildNumber'] ?? '', + ); + packageInfoList.add(packageInfo); + } catch (e) { + // Skip individual app errors but continue processing others + print('Error parsing app data: $e'); // OK + } + } + return packageInfoList; + } catch (e) { + print('Error getting installed apps: $e'); // OK + return []; + } } Future getInstalledInfo( String? packageName, { bool printErr = true, }) async { - try { + if (packageName == null || packageName.isEmpty) { return null; + } + + try { + // Use platform channel to get specific app info + const platform = MethodChannel('updatium/package_manager'); + final Map? appData = await platform.invokeMethod('getAppInfo', {'packageName': packageName}); + + if (appData == null) { + return null; + } + + return PackageInfo( + appName: appData['appName'] ?? '', + packageName: appData['packageName'] ?? '', + version: appData['version'] ?? '', + buildNumber: appData['buildNumber'] ?? '', + ); } catch (e) { if (printErr) { - print(e); // OK - } + print(e); // OK } - return null; + return null; + } } Future getAppStorageDir() async => diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart index 3998f374e..fdec789c2 100644 --- a/lib/security/security_settings_provider.dart +++ b/lib/security/security_settings_provider.dart @@ -60,6 +60,8 @@ class SecuritySettingsProvider { enableAutoUpdate: enabled, ); _scanner = YARAScanner.getInstance(_config); + // Reinitialize scanner to apply new timer settings + await _scanner.initialize(); } // Update Interval Settings @@ -73,6 +75,8 @@ class SecuritySettingsProvider { enableAutoUpdate: _config.enableAutoUpdate, ); _scanner = YARAScanner.getInstance(_config); + // Reinitialize scanner to apply new timer settings + await _scanner.initialize(); } // Threat Level Filter @@ -116,12 +120,25 @@ class SecuritySettingsProvider { throw StateError(result.error!); } - // Handle quarantine if enabled - if (result.isInfected && getQuarantineInfected()) { - await _quarantineFile(apkPath, result); + // Apply threat level filter + final threshold = getThreatLevelFilter(); + final filteredMatches = result.matches.where((match) => match.threatLevel >= threshold).toList(); + + // Create filtered result for decision making and reporting + final filteredResult = YARAScanResult( + isInfected: filteredMatches.isNotEmpty, + matches: filteredMatches, + filePath: result.filePath, + scanTime: result.scanTime, + error: result.error, + ); + + // Handle quarantine if enabled and filtered matches exist + if (filteredResult.isInfected && getQuarantineInfected()) { + await _quarantineFile(apkPath, filteredResult); } - return result; + return filteredResult; } /// Move infected file to quarantine diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index 1620b7a59..b31f81928 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -114,15 +114,74 @@ class YARARule { }); factory YARARule.fromString(String ruleContent) { + // For backward compatibility, if the content contains multiple rules, + // return the first one + final rules = YARARule.parseMultiple(ruleContent); + return rules.isNotEmpty ? rules.first : YARARule( + name: 'unknown', + content: ruleContent, + ); + } + + /// Parse multiple YARA rules from a file content + /// Returns a list of YARARule instances, one for each rule block found + static List parseMultiple(String fileContent) { + final rules = []; + final lines = fileContent.split('\n'); + + int currentRuleStart = -1; + String? currentRuleName; + final currentRuleLines = []; + + for (int i = 0; i < lines.length; i++) { + final line = lines[i]; + final trimmedLine = line.trim(); + + // Detect rule block start + if (trimmedLine.startsWith('rule ')) { + // If we were building a previous rule, finalize it first + if (currentRuleStart != -1 && currentRuleName != null) { + final ruleContent = currentRuleLines.join('\n'); + rules.add(_parseSingleRule(ruleContent, currentRuleName)); + } + + // Start new rule + currentRuleStart = i; + currentRuleName = trimmedLine.substring(5).trim().split(' ').first; + currentRuleLines.clear(); + currentRuleLines.add(line); + } else if (currentRuleStart != -1) { + // We're inside a rule block + currentRuleLines.add(line); + + // Check if this might be the end of a rule (next rule start or end of file) + if (i == lines.length - 1) { + // End of file - finalize the last rule + final ruleContent = currentRuleLines.join('\n'); + rules.add(_parseSingleRule(ruleContent, currentRuleName!)); + } + } + } + + // Handle case where file has no explicit rule blocks (single rule without "rule " prefix) + if (rules.isEmpty && fileContent.trim().isNotEmpty) { + rules.add(_parseSingleRule(fileContent, null)); + } + + return rules; + } + + /// Parse a single rule block with its content + static YARARule _parseSingleRule(String ruleContent, String? fallbackName) { final lines = ruleContent.split('\n'); - String? ruleName; + String? ruleName = fallbackName; String? author; String? description; final tags = []; for (final line in lines) { final trimmedLine = line.trim(); - if (trimmedLine.startsWith('rule ')) { + if (trimmedLine.startsWith('rule ') && ruleName == null) { ruleName = trimmedLine.substring(5).trim().split(' ').first; } else if (trimmedLine.startsWith('author = ')) { author = trimmedLine.substring(9).trim().replaceAll('"', ''); @@ -183,6 +242,19 @@ class YARAScanner { Future initialize() async { await _loadRules(); + // Check if any rules were actually loaded + if (_rules.isEmpty) { + // Trigger immediate update for fresh installs + try { + await updateRules(); + // Reload rules after update + await _loadRules(); + } catch (e) { + // Log error but continue with initialization + _logs.add('Initial rule update failed: ${e.toString()}'); + } + } + // Cancel existing timer before starting new one _updateTimer?.cancel(); @@ -207,9 +279,9 @@ class YARAScanner { if (entity is File && entity.path.endsWith('.yar')) { try { final content = await entity.readAsString(); - final rule = YARARule.fromString(content); - _rules.add(rule); - loadedCount++; + final rules = YARARule.parseMultiple(content); + _rules.addAll(rules); + loadedCount += rules.length; } catch (e) { errorCount++; // Log error without exposing sensitive file paths or rule content @@ -357,48 +429,76 @@ class YARAScanner { if (trimmedLine.startsWith('condition:')) { // condition = trimmedLine.substring(10).trim(); // Not used in basic implementation } else if (trimmedLine.contains('\$') && trimmedLine.contains(' = ')) { - final stringMatch = RegExp(r'\$(\w+)\s*=\s*{([^}]+)}').firstMatch(trimmedLine); - if (stringMatch != null && stringMatch.group(1) != null) { - strings.add(stringMatch.group(1)!); + // Extract quoted strings: $name = "text" + final quotedMatch = RegExp(r'\$(\w+)\s*=\s*["\']([^"\']+)["\']').firstMatch(trimmedLine); + if (quotedMatch != null && quotedMatch.group(1) != null) { + strings.add(quotedMatch.group(1)!); + continue; + } + + // Extract hex sequences: $name = {6A 40} or $name = 6A 40 68 + final hexMatch = RegExp(r'\$(\w+)\s*=\s*(?:{([^}]+)}|([0-9A-Fa-f\s]+))').firstMatch(trimmedLine); + if (hexMatch != null && hexMatch.group(1) != null) { + strings.add(hexMatch.group(1)!); } } } // Check if any strings match in binary data for (final string in strings) { - final stringPattern = RegExp(r'\$' + string + r'\s*=\s*{([^}]+)}'); - final stringMatch = stringPattern.firstMatch(rule.content); - if (stringMatch != null) { - final searchString = stringMatch.group(1)!.trim().replaceAll('"', ''); + // Try quoted string pattern first + String? searchString; + List searchBytes; + + final quotedPattern = RegExp(r'\$' + string + r'\s*=\s*["\']([^"\']+)["\']'); + final quotedMatch = quotedPattern.firstMatch(rule.content); + + if (quotedMatch != null) { + // Case 1: Quoted strings - strip quotes and use utf8.encode + searchString = quotedMatch.group(1)!; + searchBytes = utf8.encode(searchString); + } else { + // Try hex patterns (both braced and unbraced) + final hexPattern = RegExp(r'\$' + string + r'\s*=\s*(?:{([^}]+)}|([0-9A-Fa-f\s]+))'); + final hexMatch = hexPattern.firstMatch(rule.content); - // Convert search string to bytes for binary comparison - List searchBytes; - try { - searchBytes = utf8.encode(searchString); - } catch (e) { - // Handle hex strings like {6A 40 68 00 30 00 00} - final hexString = searchString.replaceAll(RegExp(r'[{} ]'), ''); + if (hexMatch != null) { + // Case 2: Hex sequences - parse hex pairs into bytes + final hexContent = hexMatch.group(1) ?? hexMatch.group(2)!; + final cleanHex = hexContent.replaceAll(RegExp(r'\s+'), ''); searchBytes = []; - for (int i = 0; i < hexString.length; i += 2) { - if (i + 1 < hexString.length) { - final byte = int.tryParse(hexString.substring(i, i + 2), radix: 16); + + for (int i = 0; i < cleanHex.length; i += 2) { + if (i + 1 < cleanHex.length) { + final byte = int.tryParse(cleanHex.substring(i, i + 2), radix: 16); if (byte != null) { searchBytes.add(byte); } } } + } else { + // Case 3: Default text - try to find any remaining pattern and use utf8.encode + final defaultPattern = RegExp(r'\$' + string + r'\s*=\s*([^\n]+)'); + final defaultMatch = defaultPattern.firstMatch(rule.content); + + if (defaultMatch != null) { + searchString = defaultMatch.group(1)!.trim().replaceAll(RegExp(r'^["\']|["\']$'), ''); + searchBytes = utf8.encode(searchString); + } else { + continue; // Skip if no pattern found + } } + } - // Search for bytes in the file - if (_containsBytes(fileBytes, searchBytes)) { - return YARAMatch( - ruleName: rule.name, - description: rule.description ?? 'No description available', - author: rule.author, - tags: rule.tags, - threatLevel: _calculateThreatLevel(rule.tags), - ); - } + // Search for bytes in the file + if (_containsBytes(fileBytes, searchBytes)) { + return YARAMatch( + ruleName: rule.name, + description: rule.description ?? 'No description available', + author: rule.author, + tags: rule.tags, + threatLevel: _calculateThreatLevel(rule.tags), + ); } } From 6b4551f08a801fcbf4ead6be6a368c45d265d88c Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:52:01 +0000 Subject: [PATCH 32/46] Update assets/translations/es.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- assets/translations/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/translations/es.json b/assets/translations/es.json index c3f85fce5..ee4c0cc46 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -28,7 +28,7 @@ "githubStarredRepos": "repositorios favoritos en GitHub", "uname": "Nombre de usuario", "wrongArgNum": "Número de argumentos provistos inválido", - "yaraMalwareScanner": "Escaneo Automático de Malware (Reglos Estilo YARA)", + "yaraMalwareScanner": "Escaneo Automático de Malware (Reglas Estilo YARA)", "yaraScannerDescription": "Detección básica de malware usando reglas de coincidencia de cadenas estilo YARA para protección contra amenazas conocidas. Esta implementación proporciona capacidades fundamentales de coincidencia de patrones. La responsabilidad del uso seguro de la aplicación recae finalmente en el usuario.", "securitySettings": "Configuración de Seguridad", "enableAutoScan": "Activar Escaneo Automático", From 6b4928f2b8b83d461d75e1beeb16bd560abf7fc3 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:52:18 +0000 Subject: [PATCH 33/46] Update android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../main/kotlin/io/github/omeritzics/updatium/MainActivity.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt b/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt index 843c4c6f6..7a032cdee 100644 --- a/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt +++ b/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt @@ -94,8 +94,7 @@ class MainActivity : FlutterActivity() { ) } catch (e: PackageManager.NameNotFoundException) { null - } catch (e: Exception) { - null + } } } } From 2387e6433b8fea8e05838b3e45759dcd6c2fcc05 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:52:37 +0000 Subject: [PATCH 34/46] Update android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../io/github/omeritzics/updatium/MainActivity.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt b/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt index 7a032cdee..21c142b69 100644 --- a/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt +++ b/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt @@ -67,9 +67,18 @@ class MainActivity : FlutterActivity() { ) appsList.add(appMap) } - } catch (e: Exception) { - // Skip apps that can't be accessed - } +import android.util.Log + +class MainActivity : FlutterActivity() { + private val CHANNEL = "updatium/package_manager" + private val TAG = "MainActivity" + + // ... other code ... + + } catch (e: Exception) { + Log.w(TAG, "Skipping inaccessible package entry", e) + } +} } return appsList From 57044ae47788b43674b5ed2ad2dc4ee4046d739f Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:53:01 +0000 Subject: [PATCH 35/46] Update lib/pages/settings.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/pages/settings.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 7c07d7433..a9c5a639b 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -963,7 +963,9 @@ onChanged: _securityProviderInitialized ? (value) async { value: _securityProviderInitialized ? _securityProvider.getAutoUpdateEnabled() : false, onChanged: _securityProviderInitialized ? (value) async { await _securityProvider.setAutoUpdateEnabled(value); - setState(() {}); + if (mounted) { + setState(() {}); + } } : null, ), ListTile( From a53b15bb7abfed4d850c016c7fd1bc35dbf5ed2a Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:53:27 +0000 Subject: [PATCH 36/46] Update lib/pages/settings.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/pages/settings.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index a9c5a639b..caf8def9a 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -982,10 +982,11 @@ onChanged: _securityProviderInitialized ? (value) async { onChanged: _securityProviderInitialized ? (value) async { if (value != null) { await _securityProvider.setUpdateInterval(value); - setState(() {}); + if (mounted) { + setState(() {}); + } } } : null, - ), ), height16, // Update button with error handling From cd84b434c868ca61eef1f34165ccfaf17af41980 Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 12 Apr 2026 07:56:24 +0300 Subject: [PATCH 37/46] Commit --- lib/security/yara_scanner.dart | 151 ++++++++++++++++++++------------- 1 file changed, 94 insertions(+), 57 deletions(-) diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index b31f81928..f40d8803f 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -271,7 +271,8 @@ class YARAScanner { await rulesDir.create(recursive: true); } - _rules.clear(); + // Build new rules list to avoid concurrent modification + final newRules = []; int loadedCount = 0; int errorCount = 0; @@ -280,7 +281,7 @@ class YARAScanner { try { final content = await entity.readAsString(); final rules = YARARule.parseMultiple(content); - _rules.addAll(rules); + newRules.addAll(rules); loadedCount += rules.length; } catch (e) { errorCount++; @@ -290,6 +291,9 @@ class YARAScanner { } } + // Atomically replace the shared reference + _rules.clear(); + _rules.addAll(newRules); _logs.add('YARA rules loaded: $loadedCount successful, $errorCount failed'); } catch (e) { _logs.add('Error loading YARA rules: ${e.toString()}'); @@ -396,7 +400,9 @@ class YARAScanner { final fileBytes = await file.readAsBytes(); final matches = []; - for (final rule in _rules) { + // Take snapshot of rules to avoid concurrent modifications + final rules = List.from(_rules); + for (final rule in rules) { final match = await _checkRule(rule, fileBytes); if (match != null) { matches.add(match); @@ -419,95 +425,126 @@ class YARAScanner { YARARule rule, List fileBytes, ) async { - // Simple string matching (basic implementation) - // In a real implementation, you'd want to use proper YARA parsing final ruleLines = rule.content.split('\n'); - final strings = []; + final stringPatterns = >{}; + String? condition; + bool requiresAll = false; // Default to "any of" logic + // Parse strings and condition for (final line in ruleLines) { final trimmedLine = line.trim(); + if (trimmedLine.startsWith('condition:')) { - // condition = trimmedLine.substring(10).trim(); // Not used in basic implementation + condition = trimmedLine.substring(10).trim(); + // Parse condition to determine if it requires "all" or "any" strings + requiresAll = _parseConditionLogic(condition); } else if (trimmedLine.contains('\$') && trimmedLine.contains(' = ')) { // Extract quoted strings: $name = "text" - final quotedMatch = RegExp(r'\$(\w+)\s*=\s*["\']([^"\']+)["\']').firstMatch(trimmedLine); + final quotedPattern = RegExp(r'\$(\w+)\s*=\s*["\']([^"\']*)["\']'); + final quotedMatch = quotedPattern.firstMatch(trimmedLine); if (quotedMatch != null && quotedMatch.group(1) != null) { - strings.add(quotedMatch.group(1)!); + final identifier = quotedMatch.group(1)!; + final content = quotedMatch.group(2)!; + if (content.isNotEmpty) { + stringPatterns[identifier] = utf8.encode(content); + } continue; } // Extract hex sequences: $name = {6A 40} or $name = 6A 40 68 - final hexMatch = RegExp(r'\$(\w+)\s*=\s*(?:{([^}]+)}|([0-9A-Fa-f\s]+))').firstMatch(trimmedLine); + final hexPattern = RegExp(r'\$(\w+)\s*=\s*(?:{([^}]+)}|([0-9A-Fa-f\s]+))'); + final hexMatch = hexPattern.firstMatch(trimmedLine); if (hexMatch != null && hexMatch.group(1) != null) { - strings.add(hexMatch.group(1)!); - } - } - } - - // Check if any strings match in binary data - for (final string in strings) { - // Try quoted string pattern first - String? searchString; - List searchBytes; - - final quotedPattern = RegExp(r'\$' + string + r'\s*=\s*["\']([^"\']+)["\']'); - final quotedMatch = quotedPattern.firstMatch(rule.content); - - if (quotedMatch != null) { - // Case 1: Quoted strings - strip quotes and use utf8.encode - searchString = quotedMatch.group(1)!; - searchBytes = utf8.encode(searchString); - } else { - // Try hex patterns (both braced and unbraced) - final hexPattern = RegExp(r'\$' + string + r'\s*=\s*(?:{([^}]+)}|([0-9A-Fa-f\s]+))'); - final hexMatch = hexPattern.firstMatch(rule.content); - - if (hexMatch != null) { - // Case 2: Hex sequences - parse hex pairs into bytes - final hexContent = hexMatch.group(1) ?? hexMatch.group(2)!; + final identifier = hexMatch.group(1)!; + final hexContent = hexMatch.group(2) ?? hexMatch.group(3) ?? ''; final cleanHex = hexContent.replaceAll(RegExp(r'\s+'), ''); - searchBytes = []; + final bytes = []; for (int i = 0; i < cleanHex.length; i += 2) { if (i + 1 < cleanHex.length) { final byte = int.tryParse(cleanHex.substring(i, i + 2), radix: 16); if (byte != null) { - searchBytes.add(byte); + bytes.add(byte); } } } - } else { - // Case 3: Default text - try to find any remaining pattern and use utf8.encode - final defaultPattern = RegExp(r'\$' + string + r'\s*=\s*([^\n]+)'); - final defaultMatch = defaultPattern.firstMatch(rule.content); - if (defaultMatch != null) { - searchString = defaultMatch.group(1)!.trim().replaceAll(RegExp(r'^["\']|["\']$'), ''); - searchBytes = utf8.encode(searchString); - } else { - continue; // Skip if no pattern found + if (bytes.isNotEmpty) { + stringPatterns[identifier] = bytes; } } } + } - // Search for bytes in the file - if (_containsBytes(fileBytes, searchBytes)) { - return YARAMatch( - ruleName: rule.name, - description: rule.description ?? 'No description available', - author: rule.author, - tags: rule.tags, - threatLevel: _calculateThreatLevel(rule.tags), - ); + // If no condition found, default to requiring at least one match + if (condition == null) { + requiresAll = false; + } + + // Find which string identifiers match in the file + final matchedIdentifiers = []; + for (final entry in stringPatterns.entries) { + if (_containsBytes(fileBytes, entry.value)) { + matchedIdentifiers.add(entry.key); } } + // Evaluate condition based on matched identifiers + bool ruleMatches = false; + if (matchedIdentifiers.isEmpty) { + ruleMatches = false; + } else if (requiresAll) { + // All defined strings must be found + ruleMatches = matchedIdentifiers.length == stringPatterns.length; + } else { + // At least one string must be found + ruleMatches = matchedIdentifiers.isNotEmpty; + } + + if (ruleMatches) { + return YARAMatch( + ruleName: rule.name, + description: rule.description ?? 'No description available', + author: rule.author, + tags: rule.tags, + threatLevel: _calculateThreatLevel(rule.tags), + ); + } + return null; } + /// Parse condition logic to determine if rule requires "all" or "any" strings + bool _parseConditionLogic(String condition) { + // Default to "any of" logic unless explicitly requiring all + if (condition.isEmpty) return false; + + // Look for "all of" patterns + if (condition.contains('all of') || + condition.contains('all of them') || + condition.contains('and')) { + return true; + } + + // Look for "any of" patterns + if (condition.contains('any of') || + condition.contains('any of them') || + condition.contains('or')) { + return false; + } + + // If condition is just a single identifier like "$a", default to any (requires at least one) + if (RegExp(r'^\$\w+$').hasMatch(condition.trim())) { + return false; + } + + // Default to requiring at least one match for complex conditions + return false; + } + /// Helper method to check if byte sequence contains another byte sequence bool _containsBytes(List data, List pattern) { - if (pattern.isEmpty) return true; + if (pattern.isEmpty) return false; if (data.length < pattern.length) return false; for (int i = 0; i <= data.length - pattern.length; i++) { From dedf2f83ce0b6ee11db124acff744bb1857ed85e Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 12 Apr 2026 07:58:44 +0300 Subject: [PATCH 38/46] Fix --- lib/providers/apps_provider.dart | 44 +++++-- lib/security/security_settings_provider.dart | 37 ++++-- lib/security/yara_scanner.dart | 129 +++++++++++-------- 3 files changed, 131 insertions(+), 79 deletions(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index f221f9c2a..265b22434 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -36,7 +36,6 @@ import 'package:share_plus/share_plus.dart'; import 'package:updatium/security/security_settings_provider.dart'; import 'package:package_info_plus/package_info_plus.dart'; - class AppInMemory { late App app; double? downloadProgress; @@ -496,8 +495,10 @@ Future> getAllInstalledInfo() async { try { // Use platform channel to get all installed apps const platform = MethodChannel('updatium/package_manager'); - final List installedApps = await platform.invokeMethod('getInstalledApps'); - + final List installedApps = await platform.invokeMethod( + 'getInstalledApps', + ); + List packageInfoList = []; for (var appData in installedApps) { try { @@ -528,16 +529,19 @@ Future getInstalledInfo( if (packageName == null || packageName.isEmpty) { return null; } - + try { // Use platform channel to get specific app info const platform = MethodChannel('updatium/package_manager'); - final Map? appData = await platform.invokeMethod('getAppInfo', {'packageName': packageName}); - + final Map? appData = await platform.invokeMethod( + 'getAppInfo', + {'packageName': packageName}, + ); + if (appData == null) { return null; } - + return PackageInfo( appName: appData['appName'] ?? '', packageName: appData['packageName'] ?? '', @@ -972,7 +976,10 @@ class AppsProvider with ChangeNotifier { } /// Scan APK for malware before installation - Future _scanAPKForMalware(String apkPath, {List? additionalApkPaths}) async { + Future _scanAPKForMalware( + String apkPath, { + List? additionalApkPaths, + }) async { SecuritySettingsProvider? securityProvider; try { securityProvider = await SecuritySettingsProvider.create(); @@ -993,15 +1000,21 @@ class AppsProvider with ChangeNotifier { if (additionalApkPaths != null) { for (final additionalApkPath in additionalApkPaths) { - final additionalScanResult = await securityProvider.scanAPK(additionalApkPath); + final additionalScanResult = await securityProvider.scanAPK( + additionalApkPath, + ); if (additionalScanResult.error != null) { - logs.add('Security scan error for additional APK: ${additionalScanResult.error}'); + logs.add( + 'Security scan error for additional APK: ${additionalScanResult.error}', + ); return false; } if (additionalScanResult.isInfected) { - logs.add('Security scan detected malware in additional APK: ${additionalScanResult.matches.map((m) => m.ruleName).join(', ')}'); + logs.add( + 'Security scan detected malware in additional APK: ${additionalScanResult.matches.map((m) => m.ruleName).join(', ')}', + ); return false; } } @@ -1052,9 +1065,12 @@ class AppsProvider with ChangeNotifier { } } PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); - + // Security scan before installation - if (!(await _scanAPKForMalware(file.file.path, additionalApkPaths: additionalAPKs.map((a) => a.file.path).toList()))) { + if (!(await _scanAPKForMalware( + file.file.path, + additionalApkPaths: additionalAPKs.map((a) => a.file.path).toList(), + ))) { try { if (file.file.existsSync()) { deleteFile(file.file); @@ -1069,7 +1085,7 @@ class AppsProvider with ChangeNotifier { } throw UpdatiumError(tr('securityScanBlocked')); } - + logs.add( 'Installing "${newInfo.packageName}" version "${newInfo.versionName}" versionCode "${newInfo.versionCode}"${appInfo != null ? ' (from existing version "${appInfo.versionName}" versionCode "${appInfo.versionCode}")' : ''}', ); diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart index fdec789c2..ef7784540 100644 --- a/lib/security/security_settings_provider.dart +++ b/lib/security/security_settings_provider.dart @@ -47,7 +47,8 @@ class SecuritySettingsProvider { // Auto Scan Settings bool getAutoScanEnabled() => _prefs.getBool(_keyAutoScan) ?? true; - Future setAutoScanEnabled(bool enabled) => _prefs.setBool(_keyAutoScan, enabled); + Future setAutoScanEnabled(bool enabled) => + _prefs.setBool(_keyAutoScan, enabled); // Auto Update Settings bool getAutoUpdateEnabled() => _prefs.getBool(_keyAutoUpdate) ?? true; @@ -81,23 +82,30 @@ class SecuritySettingsProvider { // Threat Level Filter int getThreatLevelFilter() => _prefs.getInt(_keyThreatLevel) ?? 1; - Future setThreatLevelFilter(int level) => _prefs.setInt(_keyThreatLevel, level); + Future setThreatLevelFilter(int level) => + _prefs.setInt(_keyThreatLevel, level); // Quarantine Settings - bool getQuarantineInfected() => _prefs.getBool(_keyQuarantineInfected) ?? true; - Future setQuarantineInfected(bool enabled) => _prefs.setBool(_keyQuarantineInfected, enabled); + bool getQuarantineInfected() => + _prefs.getBool(_keyQuarantineInfected) ?? true; + Future setQuarantineInfected(bool enabled) => + _prefs.setBool(_keyQuarantineInfected, enabled); // Last Update Tracking DateTime? getLastUpdate() { final timestamp = _prefs.getInt(_keyLastUpdate); - return timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp) : null; + return timestamp != null + ? DateTime.fromMillisecondsSinceEpoch(timestamp) + : null; } - Future setLastUpdate(DateTime update) => _prefs.setInt(_keyLastUpdate, update.millisecondsSinceEpoch); + Future setLastUpdate(DateTime update) => + _prefs.setInt(_keyLastUpdate, update.millisecondsSinceEpoch); // Rules Version Tracking String? getRulesVersion() => _prefs.getString(_keyRulesVersion); - Future setRulesVersion(String version) => _prefs.setString(_keyRulesVersion, version); + Future setRulesVersion(String version) => + _prefs.setString(_keyRulesVersion, version); /// Initialize the security scanner Future initialize() async { @@ -119,11 +127,13 @@ class SecuritySettingsProvider { if (result.error != null) { throw StateError(result.error!); } - + // Apply threat level filter final threshold = getThreatLevelFilter(); - final filteredMatches = result.matches.where((match) => match.threatLevel >= threshold).toList(); - + final filteredMatches = result.matches + .where((match) => match.threatLevel >= threshold) + .toList(); + // Create filtered result for decision making and reporting final filteredResult = YARAScanResult( isInfected: filteredMatches.isNotEmpty, @@ -132,12 +142,12 @@ class SecuritySettingsProvider { scanTime: result.scanTime, error: result.error, ); - + // Handle quarantine if enabled and filtered matches exist if (filteredResult.isInfected && getQuarantineInfected()) { await _quarantineFile(apkPath, filteredResult); } - + return filteredResult; } @@ -162,7 +172,8 @@ class SecuritySettingsProvider { await originalFile.delete(); } - final reportPath = '${quarantineDir.path}/$timestamp-$fileName-report.json'; + final reportPath = + '${quarantineDir.path}/$timestamp-$fileName-report.json'; final report = { 'originalPath': filePath, 'quarantinedPath': quarantinedPath, diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index f40d8803f..c763f96ac 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -32,12 +32,14 @@ class YARARuleUpdateError extends UpdatiumError { final List failedSources; final List successfulSources; final String details; - + YARARuleUpdateError({ required this.failedSources, required this.successfulSources, required this.details, - }) : super('Failed to update YARA rules from ${failedSources.length} sources: $details'); + }) : super( + 'Failed to update YARA rules from ${failedSources.length} sources: $details', + ); } /// YARA Scan Result @@ -117,10 +119,9 @@ class YARARule { // For backward compatibility, if the content contains multiple rules, // return the first one final rules = YARARule.parseMultiple(ruleContent); - return rules.isNotEmpty ? rules.first : YARARule( - name: 'unknown', - content: ruleContent, - ); + return rules.isNotEmpty + ? rules.first + : YARARule(name: 'unknown', content: ruleContent); } /// Parse multiple YARA rules from a file content @@ -128,15 +129,15 @@ class YARARule { static List parseMultiple(String fileContent) { final rules = []; final lines = fileContent.split('\n'); - + int currentRuleStart = -1; String? currentRuleName; final currentRuleLines = []; - + for (int i = 0; i < lines.length; i++) { final line = lines[i]; final trimmedLine = line.trim(); - + // Detect rule block start if (trimmedLine.startsWith('rule ')) { // If we were building a previous rule, finalize it first @@ -144,7 +145,7 @@ class YARARule { final ruleContent = currentRuleLines.join('\n'); rules.add(_parseSingleRule(ruleContent, currentRuleName)); } - + // Start new rule currentRuleStart = i; currentRuleName = trimmedLine.substring(5).trim().split(' ').first; @@ -153,7 +154,7 @@ class YARARule { } else if (currentRuleStart != -1) { // We're inside a rule block currentRuleLines.add(line); - + // Check if this might be the end of a rule (next rule start or end of file) if (i == lines.length - 1) { // End of file - finalize the last rule @@ -162,15 +163,15 @@ class YARARule { } } } - + // Handle case where file has no explicit rule blocks (single rule without "rule " prefix) if (rules.isEmpty && fileContent.trim().isNotEmpty) { rules.add(_parseSingleRule(fileContent, null)); } - + return rules; } - + /// Parse a single rule block with its content static YARARule _parseSingleRule(String ruleContent, String? fallbackName) { final lines = ruleContent.split('\n'); @@ -241,7 +242,7 @@ class YARAScanner { /// Initialize the scanner Future initialize() async { await _loadRules(); - + // Check if any rules were actually loaded if (_rules.isEmpty) { // Trigger immediate update for fresh installs @@ -254,10 +255,10 @@ class YARAScanner { _logs.add('Initial rule update failed: ${e.toString()}'); } } - + // Cancel existing timer before starting new one _updateTimer?.cancel(); - + if (config.enableAutoUpdate) { _startAutoUpdate(); } @@ -275,7 +276,7 @@ class YARAScanner { final newRules = []; int loadedCount = 0; int errorCount = 0; - + await for (final entity in rulesDir.list()) { if (entity is File && entity.path.endsWith('.yar')) { try { @@ -286,7 +287,9 @@ class YARAScanner { } catch (e) { errorCount++; // Log error without exposing sensitive file paths or rule content - _logs.add('Error loading YARA rule file: ${path.basename(entity.path)}'); + _logs.add( + 'Error loading YARA rule file: ${path.basename(entity.path)}', + ); } } } @@ -294,7 +297,9 @@ class YARAScanner { // Atomically replace the shared reference _rules.clear(); _rules.addAll(newRules); - _logs.add('YARA rules loaded: $loadedCount successful, $errorCount failed'); + _logs.add( + 'YARA rules loaded: $loadedCount successful, $errorCount failed', + ); } catch (e) { _logs.add('Error loading YARA rules: ${e.toString()}'); } @@ -308,14 +313,16 @@ class YARAScanner { for (final source in config.ruleSources) { try { - final response = await http.get(Uri.parse(source)).timeout( - const Duration(seconds: 30), - onTimeout: () => throw TimeoutException('Request timed out'), - ); + final response = await http + .get(Uri.parse(source)) + .timeout( + const Duration(seconds: 30), + onTimeout: () => throw TimeoutException('Request timed out'), + ); if (response.statusCode == 200) { final fileName = source.split('/').last; final localPath = path.join(config.rulesDirectory, fileName); - + final file = File(localPath); await file.writeAsString(response.body); successfulSources.add(source); @@ -326,16 +333,20 @@ class YARAScanner { failedSources.add(source); errorDetails.add('$source: $error'); // Log error without exposing full URLs - _logs.add('YARA rule update failed: ${path.basename(source)} - $error'); + _logs.add( + 'YARA rule update failed: ${path.basename(source)} - $error', + ); } } catch (e) { failedSources.add(source); errorDetails.add('$source: $e'); // Log error without exposing full URLs or stack traces - _logs.add('YARA rule update failed: ${path.basename(source)} - ${e.toString()}'); + _logs.add( + 'YARA rule update failed: ${path.basename(source)} - ${e.toString()}', + ); } } - + // Try to load the rules that were successfully updated if (successfulSources.isNotEmpty) { try { @@ -348,10 +359,12 @@ class YARAScanner { _logs.add('YARA rules loading failed after update'); } } - + // Log summary without exposing sensitive details - _logs.add('YARA rules update completed: ${successfulSources.length} successful, ${failedSources.length} failed'); - + _logs.add( + 'YARA rules update completed: ${successfulSources.length} successful, ${failedSources.length} failed', + ); + // If there were any failures, throw an exception with actionable context if (failedSources.isNotEmpty) { throw YARARuleUpdateError( @@ -375,7 +388,7 @@ class YARAScanner { level: LogLevels.error, context: 'YARAScanner._startAutoUpdate', ); - + // Also log structured information for security auditing await _logs.addStructured( operation: 'auto_update_rules', @@ -383,7 +396,7 @@ class YARAScanner { errorCode: e.toString(), level: LogLevels.error, ); - + // Optionally print in debug mode for immediate visibility if (kDebugMode) { print('YARA auto update error: $e'); @@ -421,10 +434,7 @@ class YARAScanner { } /// Check if a file matches a specific YARA rule - Future _checkRule( - YARARule rule, - List fileBytes, - ) async { + Future _checkRule(YARARule rule, List fileBytes) async { final ruleLines = rule.content.split('\n'); final stringPatterns = >{}; String? condition; @@ -433,16 +443,28 @@ class YARAScanner { // Parse strings and condition for (final line in ruleLines) { final trimmedLine = line.trim(); - + if (trimmedLine.startsWith('condition:')) { condition = trimmedLine.substring(10).trim(); // Parse condition to determine if it requires "all" or "any" strings requiresAll = _parseConditionLogic(condition); } else if (trimmedLine.contains('\$') && trimmedLine.contains(' = ')) { // Extract quoted strings: $name = "text" - final quotedPattern = RegExp(r'\$(\w+)\s*=\s*["\']([^"\']*)["\']'); - final quotedMatch = quotedPattern.firstMatch(trimmedLine); - if (quotedMatch != null && quotedMatch.group(1) != null) { + RegExpMatch? quotedMatch; + + // Try double quotes first + final doubleQuotePattern = RegExp(r'\$(\w+)\s*=\s*"([^"]*)"'); + quotedMatch = doubleQuotePattern.firstMatch(trimmedLine); + + // If no double quote match, try single quotes + if (quotedMatch == null) { + final singleQuotePattern = RegExp(r'\$(\w+)\s*=\s*' + r"'([^\']*)'"); + quotedMatch = singleQuotePattern.firstMatch(trimmedLine); + } + + if (quotedMatch != null && + quotedMatch.group(1) != null && + quotedMatch.group(2) != null) { final identifier = quotedMatch.group(1)!; final content = quotedMatch.group(2)!; if (content.isNotEmpty) { @@ -450,25 +472,30 @@ class YARAScanner { } continue; } - + // Extract hex sequences: $name = {6A 40} or $name = 6A 40 68 - final hexPattern = RegExp(r'\$(\w+)\s*=\s*(?:{([^}]+)}|([0-9A-Fa-f\s]+))'); + final hexPattern = RegExp( + r'\$(\w+)\s*=\s*(?:{([^}]+)}|([0-9A-Fa-f\s]+))', + ); final hexMatch = hexPattern.firstMatch(trimmedLine); if (hexMatch != null && hexMatch.group(1) != null) { final identifier = hexMatch.group(1)!; final hexContent = hexMatch.group(2) ?? hexMatch.group(3) ?? ''; final cleanHex = hexContent.replaceAll(RegExp(r'\s+'), ''); final bytes = []; - + for (int i = 0; i < cleanHex.length; i += 2) { if (i + 1 < cleanHex.length) { - final byte = int.tryParse(cleanHex.substring(i, i + 2), radix: 16); + final byte = int.tryParse( + cleanHex.substring(i, i + 2), + radix: 16, + ); if (byte != null) { bytes.add(byte); } } } - + if (bytes.isNotEmpty) { stringPatterns[identifier] = bytes; } @@ -520,14 +547,14 @@ class YARAScanner { if (condition.isEmpty) return false; // Look for "all of" patterns - if (condition.contains('all of') || + if (condition.contains('all of') || condition.contains('all of them') || condition.contains('and')) { return true; } // Look for "any of" patterns - if (condition.contains('any of') || + if (condition.contains('any of') || condition.contains('any of them') || condition.contains('or')) { return false; @@ -574,7 +601,7 @@ class YARAScanner { try { final file = File(filePath); final bytes = await file.readAsBytes(); - + final md5Hash = md5.convert(bytes); final sha1Hash = sha1.convert(bytes); final sha256Hash = sha256.convert(bytes); @@ -585,9 +612,7 @@ class YARAScanner { 'sha256': sha256Hash.toString(), }; } catch (e) { - return { - 'error': 'Failed to calculate hashes: $e', - }; + return {'error': 'Failed to calculate hashes: $e'}; } } From 96ec94007b8c3f1ea13bda455985d8f832ee2d1b Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 12 Apr 2026 08:03:01 +0300 Subject: [PATCH 39/46] Update --- assets/translations/fa.json | 31 +++++ assets/translations/he.json | 35 ++++- assets/translations/hu.json | 31 +++++ assets/translations/zh-Hant-TW.json | 31 +++++ lib/security/yara_scanner.dart | 199 ++++++++++++++++++++++++++-- 5 files changed, 317 insertions(+), 10 deletions(-) diff --git a/assets/translations/fa.json b/assets/translations/fa.json index cf1e46ddb..e833b4c04 100644 --- a/assets/translations/fa.json +++ b/assets/translations/fa.json @@ -28,6 +28,37 @@ "githubStarredRepos": "مخازن ستاره دار گیتهاب", "uname": "نام کاربری", "wrongArgNum": "تعداد آرگومان های ارائه شده اشتباه است", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} فقط ردیابی", "source": "منبع", "app": "برنامه", diff --git a/assets/translations/he.json b/assets/translations/he.json index 776b4fdb7..3201e5283 100644 --- a/assets/translations/he.json +++ b/assets/translations/he.json @@ -27,8 +27,39 @@ "useMaterialYou": "שימוש בצבעי Material You", "githubStarredRepos": "מאגרי GitHub מסומנים בכוכב", "uname": "שם משתמש", - "wrongArgNum": "סופק מספר ארגומנטים שגוי", - "xIsTrackOnly": "המקור {} מיועד למעקב עדכונים בלבד", + "wrongArgNum": "ספק ×ספר ×ר××× ×× ×©××", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", + "xIsTrackOnly": "××§×ר {} ×××× ×××§× ×¢×××× ×× ×××", "source": "מקור", "app": "יישום", "appsFromSourceAreTrackOnly": "יישומים ממקור זה ניתנים למעקב עדכונים בלבד.", diff --git a/assets/translations/hu.json b/assets/translations/hu.json index c4b9c81bb..12b7e097e 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Csillagozott GitHub-tárolók", "uname": "Felhasználónév", "wrongArgNum": "A megadott argumentumok száma nem megfelelő", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "A(z) {} csak nyomon követhető", "source": "Forrás", "app": "Alkalmazás", diff --git a/assets/translations/zh-Hant-TW.json b/assets/translations/zh-Hant-TW.json index aa9af638e..b007795f9 100644 --- a/assets/translations/zh-Hant-TW.json +++ b/assets/translations/zh-Hant-TW.json @@ -28,6 +28,37 @@ "githubStarredRepos": "GitHub 打星星的專案", "uname": "使用者名稱", "wrongArgNum": "提供的參數數量錯誤", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} 是僅追蹤", "source": "來源", "app": "應用程式", diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index c763f96ac..57dedb303 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -20,13 +20,57 @@ class YARAConfig { this.updateInterval = const Duration(hours: 24), this.enableAutoUpdate = true, this.ruleSources = const [ - 'https://raw.githubusercontent.com/Yara-Rules/rules/master/index.yar', - 'https://raw.githubusercontent.com/Yara-Rules/rules/master/malware/MALW_YaraRule_APT.yar', - 'https://raw.githubusercontent.com/Yara-Rules/rules/master/malware/MALW_YaraRule_Mobile.yar', + 'https://raw.githubusercontent.com/Yara-Rules/rules/0f93570194a80d2f2032869055808b0ddcdfb360/index.yar', + 'https://raw.githubusercontent.com/Yara-Rules/rules/0f93570194a80d2f2032869055808b0ddcdfb360/malware/MALW_YaraRule_APT.yar', + 'https://raw.githubusercontent.com/Yara-Rules/rules/0f93570194a80d2f2032869055808b0ddcdfb360/malware/MALW_YaraRule_Mobile.yar', ], }); } +/// YARA Rule Manifest +class YARARuleManifest { + final Map fileHashes; + final String signature; + final DateTime timestamp; + final String version; + + YARARuleManifest({ + required this.fileHashes, + required this.signature, + required this.timestamp, + required this.version, + }); + + factory YARARuleManifest.fromJson(Map json) { + return YARARuleManifest( + fileHashes: Map.from(json['fileHashes'] ?? {}), + signature: json['signature'] ?? '', + timestamp: DateTime.parse( + json['timestamp'] ?? DateTime.now().toIso8601String(), + ), + version: json['version'] ?? '1.0', + ); + } + + Map toJson() { + return { + 'fileHashes': fileHashes, + 'signature': signature, + 'timestamp': timestamp.toIso8601String(), + 'version': version, + }; + } +} + +/// YARA Rule Verification Exception +class YARARuleVerificationError extends UpdatiumError { + final String source; + final String reason; + + YARARuleVerificationError({required this.source, required this.reason}) + : super('YARA rule verification failed for $source: $reason'); +} + /// YARA Rule Update Exception class YARARuleUpdateError extends UpdatiumError { final List failedSources; @@ -281,6 +325,27 @@ class YARAScanner { if (entity is File && entity.path.endsWith('.yar')) { try { final content = await entity.readAsString(); + final fileName = path.basename(entity.path); + + // Find the corresponding source URL for this file + final sourceUrl = config.ruleSources.firstWhere( + (url) => url.endsWith(fileName), + orElse: () => '', + ); + + // Verify the rule file if we have a source URL + if (sourceUrl.isNotEmpty) { + try { + await _verifyRuleFile(sourceUrl, content); + } catch (e) { + errorCount++; + _logs.add( + 'Rule verification failed: $fileName - ${e.toString()}', + ); + continue; // Skip this file + } + } + final rules = YARARule.parseMultiple(content); newRules.addAll(rules); loadedCount += rules.length; @@ -305,6 +370,109 @@ class YARAScanner { } } + /// Verify YARA rule file against manifest + Future _verifyRuleFile(String source, String content) async { + try { + // Calculate SHA256 hash of the content + final contentBytes = utf8.encode(content); + final contentHash = sha256.convert(contentBytes).toString(); + + // Try to fetch manifest for this source + final manifestUrl = _getManifestUrl(source); + if (manifestUrl == null) { + _logs.add( + 'No manifest URL available for $source - skipping verification', + ); + return true; // Allow if no manifest available + } + + final manifestResponse = await http + .get(Uri.parse(manifestUrl)) + .timeout( + const Duration(seconds: 10), + onTimeout: () => + throw TimeoutException('Manifest request timed out'), + ); + + if (manifestResponse.statusCode != 200) { + throw YARARuleVerificationError( + source: source, + reason: 'Manifest HTTP ${manifestResponse.statusCode}', + ); + } + + final manifestData = jsonDecode(manifestResponse.body); + final manifest = YARARuleManifest.fromJson(manifestData); + + // Get the expected hash for this file + final fileName = source.split('/').last; + final expectedHash = manifest.fileHashes[fileName]; + + if (expectedHash == null) { + throw YARARuleVerificationError( + source: source, + reason: 'File not found in manifest', + ); + } + + // Compare hashes + if (contentHash != expectedHash) { + throw YARARuleVerificationError( + source: source, + reason: 'Hash mismatch: expected $expectedHash, got $contentHash', + ); + } + + // Verify signature (basic implementation - in production, use proper cryptographic verification) + if (!_verifySignature(manifest, manifestData)) { + throw YARARuleVerificationError( + source: source, + reason: 'Invalid manifest signature', + ); + } + + _logs.add('Rule verification passed: $fileName'); + return true; + } catch (e) { + if (e is YARARuleVerificationError) { + rethrow; + } + throw YARARuleVerificationError( + source: source, + reason: 'Verification failed: ${e.toString()}', + ); + } + } + + /// Get manifest URL for a given rule source + String? _getManifestUrl(String source) { + // For GitHub raw content, construct manifest URL + if (source.contains('raw.githubusercontent.com')) { + final uri = Uri.parse(source); + final segments = uri.pathSegments; + if (segments.length >= 4) { + // Extract owner, repo, and commit from path like /Yara-Rules/rules/commit/file.yar + final owner = segments[1]; + final repo = segments[2]; + final commit = segments[3]; + + return 'https://raw.githubusercontent.com/$owner/$repo/$commit/yara_rules_manifest.json'; + } + } + return null; + } + + /// Basic signature verification (placeholder - implement proper cryptographic verification) + bool _verifySignature( + YARARuleManifest manifest, + Map manifestData, + ) { + // In a production environment, implement proper digital signature verification + // using public key cryptography (e.g., RSA, Ed25519) + // For now, we'll do a basic check that signature exists and is not empty + return manifest.signature.isNotEmpty && manifest.signature.length > 10; + } + /// Update rules from remote sources Future updateRules() async { final List failedSources = []; @@ -323,11 +491,26 @@ class YARAScanner { final fileName = source.split('/').last; final localPath = path.join(config.rulesDirectory, fileName); - final file = File(localPath); - await file.writeAsString(response.body); - successfulSources.add(source); - // Log success without exposing full file paths - _logs.add('YARA rule updated: $fileName'); + // Verify the rule file before saving + try { + await _verifyRuleFile(source, response.body); + + final file = File(localPath); + await file.writeAsString(response.body); + successfulSources.add(source); + // Log success without exposing full file paths + _logs.add('YARA rule updated and verified: $fileName'); + } catch (e) { + if (e is YARARuleVerificationError) { + failedSources.add(source); + errorDetails.add('$source: ${e.reason}'); + _logs.add( + 'YARA rule verification failed: ${path.basename(source)} - ${e.reason}', + ); + } else { + rethrow; + } + } } else { final error = 'HTTP ${response.statusCode}: ${response.reasonPhrase}'; failedSources.add(source); From de807012eb62431d1ae368dc1d31e0132ccbc248 Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 12 Apr 2026 08:08:37 +0300 Subject: [PATCH 40/46] Add strings --- assets/translations/bs.json | 31 ++++++++++++++++++++++++++++++ assets/translations/ca.json | 31 ++++++++++++++++++++++++++++++ assets/translations/cs.json | 31 ++++++++++++++++++++++++++++++ assets/translations/da.json | 31 ++++++++++++++++++++++++++++++ assets/translations/en-EO.json | 31 ++++++++++++++++++++++++++++++ assets/translations/et.json | 31 ++++++++++++++++++++++++++++++ assets/translations/gl.json | 31 ++++++++++++++++++++++++++++++ assets/translations/id.json | 31 ++++++++++++++++++++++++++++++ assets/translations/ko.json | 35 ++++++++++++++++++++++++++++++++-- assets/translations/ml.json | 31 ++++++++++++++++++++++++++++++ assets/translations/nl.json | 31 ++++++++++++++++++++++++++++++ assets/translations/pl.json | 31 ++++++++++++++++++++++++++++++ assets/translations/pt-BR.json | 31 ++++++++++++++++++++++++++++++ assets/translations/sv.json | 31 ++++++++++++++++++++++++++++++ assets/translations/tr.json | 31 ++++++++++++++++++++++++++++++ assets/translations/uk.json | 32 ++++++++++++++++++++++++++++++- assets/translations/vi.json | 35 ++++++++++++++++++++++++++++++++-- 17 files changed, 531 insertions(+), 5 deletions(-) diff --git a/assets/translations/bs.json b/assets/translations/bs.json index 9878f239b..c2c81db7f 100644 --- a/assets/translations/bs.json +++ b/assets/translations/bs.json @@ -28,6 +28,37 @@ "githubStarredRepos": "GitHub repo-i sa zvjezdicom", "uname": "Korisničko ime", "wrongArgNum": "Naveden je pogrešan broj argumenata", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} je samo za praćenje", "source": "Izvor", "app": "Aplikacija. ", diff --git a/assets/translations/ca.json b/assets/translations/ca.json index 225c530b0..31b61dad7 100644 --- a/assets/translations/ca.json +++ b/assets/translations/ca.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Repositoris favorits de GitHub", "uname": "Nom d'usuari", "wrongArgNum": "Nombre d'arguments proveït invàlid", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} és només per a seguiment", "source": "Font", "app": "Aplicació", diff --git a/assets/translations/cs.json b/assets/translations/cs.json index 0e69a9d17..ed55eab4f 100644 --- a/assets/translations/cs.json +++ b/assets/translations/cs.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Repozitáře na GitHubu označené hvězdičkou", "uname": "Uživatelské jméno", "wrongArgNum": "Nesprávný počet zadaných argumentů", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} je určeno pouze pro sledování", "source": "Zdroj", "app": "Aplikace", diff --git a/assets/translations/da.json b/assets/translations/da.json index 44024b257..4311d41b5 100644 --- a/assets/translations/da.json +++ b/assets/translations/da.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Stjernemarkeret GitHub-repos", "uname": "Brugernavn", "wrongArgNum": "Forkert antal argumenter angivet", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} er 'Følg Kun'", "source": "Kilde", "app": "App", diff --git a/assets/translations/en-EO.json b/assets/translations/en-EO.json index 2f96bc18f..95be399ca 100644 --- a/assets/translations/en-EO.json +++ b/assets/translations/en-EO.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Stelaj GitHub-deponejoj", "uname": "Uzantnomo", "wrongArgNum": "Malĝusta nombro da provizitaj argumentoj", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} estas nur sekvitaj", "source": "Fonto", "app": "Apo", diff --git a/assets/translations/et.json b/assets/translations/et.json index aa2a9b505..b88d51731 100644 --- a/assets/translations/et.json +++ b/assets/translations/et.json @@ -28,6 +28,37 @@ "githubStarredRepos": "GitHub'i tärniga repod", "uname": "Kasutajanimi", "wrongArgNum": "Vale arv argumente antud", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Turvaseaded", + "enableAutoScan": "Luba automaatne skaneerimine", + "enableAutoScanDescription": "Skaneeri automaatselt alla laaditud APK-sid malware'i jaoks enne installimist", + "enableAutoUpdate": "Luba automaatne uuendamine", + "enableAutoUpdateDescription": "Uuenda automaatselt malware'i definitsioonide andmebaasi", + "updateInterval": "Uuendamise intervall", + "updateIntervalDescription": "Kui sageli uuendada malware'i definitsioone", + "hours": "tundi", + "threatLevelFilter": "Ohu taseme filter", + "threatLevelFilterDescription": "Miinimum ohu tase, mis põhjustab hoiatusi", + "level1": "Tase 1", + "lowThreat": "Madal ohu tase", + "level2": "Tase 2", + "mediumThreat": "Keskmine ohu tase", + "level3": "Tase 3", + "highThreat": "Kõrge ohu tase", + "quarantineSettings": "Karantiini seaded", + "quarantineInfected": "Karantiiniga nakatunud failid", + "quarantineInfectedDescription": "Nakka tunduvad failid karantiini", + "viewQuarantine": "Vaata karantiini", + "viewQuarantineDescription": "Haldage karantiinis olevaid faile", + "databaseInformation": "Andmebaasi teave", + "lastUpdate": "Viimane uuendus", + "rulesVersion": "Reeglite versioon", + "updateNow": "Uuenda nüüd", + "updating": "Uuendamine...", + "rulesUpdatedSuccessfully": "Malware'i definitsioonid uuendatud edukalt", + "rulesUpdateFailed": "Malware'i definitsioonide uuendamine ebaõnnestus", + "quarantineViewComingSoon": "Karantiini vaade tuleb varsti", "xIsTrackOnly": "{} on ainult jälgimiseks", "source": "Allikas", "app": "Äpp", diff --git a/assets/translations/gl.json b/assets/translations/gl.json index 4ab97f1bc..d78de5466 100644 --- a/assets/translations/gl.json +++ b/assets/translations/gl.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Repositorios GitHub con estrela", "uname": "Identificador", "wrongArgNum": "Número de argumentos proporcionados incorrecto", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} é de só-seguimento", "source": "Fonte", "app": "App", diff --git a/assets/translations/id.json b/assets/translations/id.json index 583929056..d3c1035df 100644 --- a/assets/translations/id.json +++ b/assets/translations/id.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Repositori berbintang GitHub", "uname": "Nama pengguna", "wrongArgNum": "Salah memberikan jumlah argumen", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} adalah Pelacakan Saja", "source": "Sumber", "app": "Aplikasi", diff --git a/assets/translations/ko.json b/assets/translations/ko.json index e22276a95..93e06f9b8 100644 --- a/assets/translations/ko.json +++ b/assets/translations/ko.json @@ -27,8 +27,39 @@ "useMaterialYou": "Material You 색상 사용", "githubStarredRepos": "GitHub 즐겨찾기 저장소", "uname": "사용자 이름", - "wrongArgNum": "잘못된 인수 수 제공", - "xIsTrackOnly": "{}는 추적 전용입니다", + "wrongArgNum": "\uc798\ubabb\ub41c \uc778\uc218 \uc218 \uc81c\uacf5", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", + "xIsTrackOnly": "{}\ub294 \ucd94\uc801 \uc804\uc6a9\uc785\ub2c8\ub2e4", "source": "소스", "app": "앱", "appsFromSourceAreTrackOnly": "이 소스의 앱은 '추적 전용'입니다.", diff --git a/assets/translations/ml.json b/assets/translations/ml.json index 5eb847c85..585f53efa 100644 --- a/assets/translations/ml.json +++ b/assets/translations/ml.json @@ -28,6 +28,37 @@ "githubStarredRepos": "GitHub സ്റ്റാർ ചെയ്ത റെപ്പോസിറ്ററികൾ", "uname": "ഉപയോക്തൃനാമം", "wrongArgNum": "തെറ്റായ എണ്ണം ആർഗ്യുമെന്റുകൾ നൽകി", + "yaraMalwareScanner": "ഓട്ടോ മാൽവെയർ സ്കാനിംഗ് (YARA-സ്റ്റൈൽ നിയമങ്ങൾ)", + "yaraScannerDescription": "അറിയപ്പെടുന്ന ഭീഷണികൾക്കെതിരെ സംരക്ഷണത്തിനായി യാര-സ്റ്റൈൽ സ്ട്രിംഗ് പരന്ന നിയമങ്ങൾ ഉപയോഗിച്ചുള്ള അടിസ്ഥാന മാൽവെയർ കണ്ടെത്തൽ. ഈ നടപ്പിലെടുക്കൽ അടിസ്ഥാന പാറ്റേൺ പരന്ന ശേഷികൾ നൽകുന്നു. സുരക്ഷിതമായ ആപ്പ് ഉപയോഗത്തിനുള്ള ഉത്തരവാദിത്തം അവസാനം ഉപയോക്താവിനാണ്.", + "securitySettings": "സുരക്ഷാ ക്രമീകരണങ്ങൾ", + "enableAutoScan": "ഓട്ടോ സ്കാൻ സജ്ജമാക്കുക", + "enableAutoScanDescription": "ഇൻസ്റ്റാളേഷൻ മുമ്പ് ഡൗൺലോഡ് ചെയ്ത എപികെകൾക്ക് മാൽവെയർ സ്കാൻ ചെയ്യുക", + "enableAutoUpdate": "ഓട്ടോ അപ്‌ഡേറ്റ് സജ്ജമാക്കുക", + "enableAutoUpdateDescription": "മാൽവെയർ നിർവചന ഡാറ്റാബേസ് ഓട്ടോമാറ്റിക്കായി അപ്‌ഡേറ്റ് ചെയ്യുക", + "updateInterval": "അപ്‌ഡേറ്റ് ഇടവേള", + "updateIntervalDescription": "പുതിയ മാൽവെയർ നിർവചനങ്ങൾക്കായി എത്രയും വേഗം പരിശോധിക്കണം", + "hours": "മണിക്കൂറുകൾ", + "threatLevelFilter": "ഭീഷണി തലം ഫിൽട്ടർ", + "threatLevelFilterDescription": "അറിയിപ്പുകൾക്ക് കാരണമാകുന്ന കുറഞ്ഞ ഭീഷണി തലം", + "level1": "തലം 1", + "lowThreat": "കുറഞ്ഞ ഭീഷണി", + "level2": "തലം 2", + "mediumThreat": "മധ്യമ ഭീഷണി", + "level3": "തലം 3", + "highThreat": "ഉയർന്ന ഭീഷണി", + "quarantineSettings": "ക്വാറന്റൈൻ ക്രമീകരണങ്ങൾ", + "quarantineInfected": "ബാധിത ഫയലുകൾ ക്വാറന്റൈൻ ചെയ്യുക", + "quarantineInfectedDescription": "കണ്ടെത്തിയ മാൽവെയറിനെ സുരക്ഷിതമായി സൂക്ഷിക്കുന്നതിന് ക്വാറന്റൈനിലേക്ക് മാറ്റുക", + "viewQuarantine": "ക്വാറന്റൈൻ കാണുക", + "viewQuarantineDescription": "ക്വാറന്റൈൻ ചെയ്ത ഫയലുകൾ കൈകാര്യം ചെയ്യുക", + "databaseInformation": "ഡാറ്റാബേസ് വിവരങ്ങൾ", + "lastUpdate": "അവസാന അപ്‌ഡേറ്റ്", + "rulesVersion": "നിയമ പതിപ്പ്", + "updateNow": "ഇപ്പോൾ അപ്‌ഡേറ്റ് ചെയ്യുക", + "updating": "അപ്‌ഡേറ്റ് ചെയ്യുന്നത്...", + "rulesUpdatedSuccessfully": "മാൽവെയർ നിർവചനങ്ങൾ വിജയകരമായി അപ്‌ഡേറ്റ് ചെയ്തു", + "rulesUpdateFailed": "മാൽവെയർ നിർവചനങ്ങൾ അപ്‌ഡേറ്റ് ചെയ്യാൻ കഴിഞ്ഞില്ല", + "quarantineViewComingSoon": "ക്വാറന്റൈൻ കാഴ്ച ഉടൻ വരും", "xIsTrackOnly": "{} ട്രാക്ക്-മാത്രം ആണ്", "source": "ഉറവിടം", "app": "ആപ്പ്", diff --git a/assets/translations/nl.json b/assets/translations/nl.json index 03df52e32..19dda2187 100644 --- a/assets/translations/nl.json +++ b/assets/translations/nl.json @@ -28,6 +28,37 @@ "githubStarredRepos": "GitHub-repo's met ster", "uname": "Gebruikersnaam", "wrongArgNum": "Incorrect aantal argumenten.", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} is 'Alleen volgen'", "source": "Bron", "app": "App", diff --git a/assets/translations/pl.json b/assets/translations/pl.json index 04609830e..535fe3b35 100644 --- a/assets/translations/pl.json +++ b/assets/translations/pl.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Repozytoria GitHub oznaczone gwiazdką", "uname": "Nazwa użytkownika", "wrongArgNum": "Nieprawidłowa liczba podanych argumentów", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} jest tylko obserwowane", "source": "Źródło", "app": "Aplikacja", diff --git a/assets/translations/pt-BR.json b/assets/translations/pt-BR.json index 2347f2e87..e8c72efee 100644 --- a/assets/translations/pt-BR.json +++ b/assets/translations/pt-BR.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Repositórios com estrela do GitHub", "uname": "Nome de usuário", "wrongArgNum": "Número errado de argumentos fornecidos", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} é somente de rastreio", "source": "Fonte", "app": "Aplicativo", diff --git a/assets/translations/sv.json b/assets/translations/sv.json index af171780a..06b300556 100644 --- a/assets/translations/sv.json +++ b/assets/translations/sv.json @@ -28,6 +28,37 @@ "githubStarredRepos": "GitHub Stjärnmärkta Förråd", "uname": "Användarnamn", "wrongArgNum": "Fel antal argument har angetts", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} är 'Följ-Endast'", "source": "Källa", "app": "App", diff --git a/assets/translations/tr.json b/assets/translations/tr.json index 980c98193..4f8030b6a 100644 --- a/assets/translations/tr.json +++ b/assets/translations/tr.json @@ -28,6 +28,37 @@ "githubStarredRepos": "Yıldızlanmış GitHub depoları", "uname": "Kullanıcı adı", "wrongArgNum": "Yanlış sayıda argüman sağlandı", + "yaraMalwareScanner": "Otomatik Kötü Amaçlı Yazılım Taraması (YARA tarzı kurallar)", + "yaraScannerDescription": "Bilinen tehditlere karşı korumada temel kötü amaçlı yazılım algılama. Bu uygulama, temel desen eşleme yetenekleri sağlar. Güvenli uygulama kullanımı sorumluluğu kullanıcıya aittir.", + "securitySettings": "Güvenlik Ayarları", + "enableAutoScan": "Otomatik Taramayı Etkinleştir", + "enableAutoScanDescription": "Yüklemeden önce indirilen APK'leri kötü amaçlı yazılımdan koruma için otomatik olarak tarayın", + "enableAutoUpdate": "Otomatik Güncellemeyi Etkinleştir", + "enableAutoUpdateDescription": "Kötü amaçlı yazılım tanımlama veritabanını otomatik olarak güncelleyin", + "updateInterval": "Güncelleme Aralığı", + "updateIntervalDescription": "Yeni kötü amaçlı yazılım tanımları için ne sıklıkla kontrol edilecek", + "hours": "saat", + "threatLevelFilter": "Tehdit Düzeyi Filtresi", + "threatLevelFilterDescription": "Uyarıları tetiklemek için minimum tehdit düzeyi", + "level1": "Seviye 1", + "lowThreat": "Düşük Tehdit", + "level2": "Seviye 2", + "mediumThreat": "Orta Düzey Tehdit", + "level3": "Seviye 3", + "highThreat": "Yüksek Tehdit", + "quarantineSettings": "Karantina Ayarları", + "quarantineInfected": "Enfekte Dosyaları Karantinaya Al", + "quarantineInfectedDescription": "Algılanan kötü amaçlı yazılımları güvenliği sağlamak için karantinaya taşı", + "viewQuarantine": "Karantinayı Görüntüle", + "viewQuarantineDescription": "Karantinaya alınan dosyaları yönet", + "databaseInformation": "Veritabanı Bilgileri", + "lastUpdate": "Son Güncelleme", + "rulesVersion": "Kurallar Sürümü", + "updateNow": "Şimdi Güncelle", + "updating": "Güncelleniyor...", + "rulesUpdatedSuccessfully": "Kötü amaçlı yazılım tanımları başarıyla güncellendi", + "rulesUpdateFailed": "Kötü amaçlı yazılım tanımlarının güncellenmesi başarısız", + "quarantineViewComingSoon": "Karantina görünümü yakında", "xIsTrackOnly": "{} sadece takip ediliyor (track-only)", "source": "Kaynak", "app": "Uygulama", diff --git a/assets/translations/uk.json b/assets/translations/uk.json index 9161f02fc..8446580e1 100644 --- a/assets/translations/uk.json +++ b/assets/translations/uk.json @@ -28,8 +28,38 @@ "githubStarredRepos": "Відзначені репозиторії GitHub", "uname": "Ім'я користувача", "wrongArgNum": "Надано неправильну кількість аргументів", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", "xIsTrackOnly": "{} - тільки відстежування", - "source": "Джерело", "app": "застосунок", "appsFromSourceAreTrackOnly": "Застосунки з цього джерела є лише для відстежування.", "youPickedTrackOnly": "Ви вибрали опцію лише для відстежування.", diff --git a/assets/translations/vi.json b/assets/translations/vi.json index ae951d88b..9708ec03c 100644 --- a/assets/translations/vi.json +++ b/assets/translations/vi.json @@ -27,8 +27,39 @@ "useMaterialYou": "Sử dụng màu Material You", "githubStarredRepos": "Kho lưu trữ có gắn dấu sao GitHub", "uname": "Tên người dùng", - "wrongArgNum": "Số lượng đối số được cung cấp sai", - "xIsTrackOnly": "{} là Chỉ theo dõi", + "wrongArgNum": "S\u1ed1 l\u01b0\u1ee3ng \u0111\u1ed1i s\u1ed1 \u0111\u01b0\u1ee3c cung c\u1ea5p sai", + "yaraMalwareScanner": "Auto Malware Scanning (YARA-style Rules)", + "yaraScannerDescription": "Basic malware detection using YARA-style string matching rules for protection against known threats. This implementation provides fundamental pattern matching capabilities. The responsibility for safe app usage ultimately is on the user.", + "securitySettings": "Security Settings", + "enableAutoScan": "Enable Auto-Scan", + "enableAutoScanDescription": "Automatically scan downloaded APKs for malware before installation", + "enableAutoUpdate": "Enable Auto-Update", + "enableAutoUpdateDescription": "Automatically update malware definition database", + "updateInterval": "Update Interval", + "updateIntervalDescription": "How often to check for new malware definitions", + "hours": "hours", + "threatLevelFilter": "Threat Level Filter", + "threatLevelFilterDescription": "Minimum threat level to trigger alerts", + "level1": "Level 1", + "lowThreat": "Low Threat", + "level2": "Level 2", + "mediumThreat": "Medium Threat", + "level3": "Level 3", + "highThreat": "High Threat", + "quarantineSettings": "Quarantine Settings", + "quarantineInfected": "Quarantine Infected Files", + "quarantineInfectedDescription": "Move detected malware to quarantine for safety", + "viewQuarantine": "View Quarantine", + "viewQuarantineDescription": "Manage quarantined files", + "databaseInformation": "Database Information", + "lastUpdate": "Last Update", + "rulesVersion": "Rules Version", + "updateNow": "Update Now", + "updating": "Updating...", + "rulesUpdatedSuccessfully": "Malware definitions updated successfully", + "rulesUpdateFailed": "Failed to update malware definitions", + "quarantineViewComingSoon": "Quarantine view coming soon", + "xIsTrackOnly": "{} l\u00e0 Ch\u1ec9 theo d\u00f5i", "source": "Nguồn", "app": "Ứng dụng", "appsFromSourceAreTrackOnly": "Các ứng dụng từ nguồn này là 'Chỉ theo dõi'.", From 20c9bcbf686374ef851e3ec926162968707035db Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 12 Apr 2026 23:02:22 +0300 Subject: [PATCH 41/46] Update --- lib/providers/apps_provider.dart | 69 +++++++----------------------- lib/providers/native_provider.dart | 8 ---- lib/security/yara_scanner.dart | 41 +++++++++++++----- 3 files changed, 46 insertions(+), 72 deletions(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 0fd868167..a5cc9bf79 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -34,7 +34,8 @@ import 'package:android_intent_plus/android_intent.dart'; import 'package:flutter_archive/flutter_archive.dart'; import 'package:share_plus/share_plus.dart'; import 'package:updatium/security/security_settings_provider.dart'; -import 'package:package_info_plus/package_info_plus.dart'; +import 'package:android_package_manager/android_package_manager.dart'; +import 'package:shizuku_apk_installer/shizuku_apk_installer.dart'; class AppInMemory { late App app; @@ -493,29 +494,9 @@ Future downloadFile( Future> getAllInstalledInfo() async { try { - // Use platform channel to get all installed apps - const platform = MethodChannel('updatium/package_manager'); - final List installedApps = await platform.invokeMethod( - 'getInstalledApps', - ); - - List packageInfoList = []; - for (var appData in installedApps) { - try { - // Create PackageInfo objects from the platform data - final packageInfo = PackageInfo( - appName: appData['appName'] ?? '', - packageName: appData['packageName'] ?? '', - version: appData['version'] ?? '', - buildNumber: appData['buildNumber'] ?? '', - ); - packageInfoList.add(packageInfo); - } catch (e) { - // Skip individual app errors but continue processing others - print('Error parsing app data: $e'); // OK - } - } - return packageInfoList; + final pm = AndroidPackageManager(); + final packages = await pm.getInstalledPackages(); + return packages ?? []; } catch (e) { print('Error getting installed apps: $e'); // OK return []; @@ -531,23 +512,8 @@ Future getInstalledInfo( } try { - // Use platform channel to get specific app info - const platform = MethodChannel('updatium/package_manager'); - final Map? appData = await platform.invokeMethod( - 'getAppInfo', - {'packageName': packageName}, - ); - - if (appData == null) { - return null; - } - - return PackageInfo( - appName: appData['appName'] ?? '', - packageName: appData['packageName'] ?? '', - version: appData['version'] ?? '', - buildNumber: appData['buildNumber'] ?? '', - ); + final pm = AndroidPackageManager(); + return await pm.getPackageInfo(packageName: packageName); } catch (e) { if (printErr) { print(e); // OK @@ -567,6 +533,7 @@ class AppsProvider with ChangeNotifier { bool gettingUpdates = false; bool exportInProgress = false; LogsProvider logs = LogsProvider(); + late AndroidPackageManager pm = AndroidPackageManager(); // Variables to keep track of the app foreground status (installs can't run in the background) bool isForeground = true; @@ -1109,9 +1076,10 @@ class AppsProvider with ChangeNotifier { int? code; if (!settingsProvider.useShizuku) { // AndroidPackageInstaller functionality removed - code = 'not_implemented'; + code = null; } else { - code = await ShizukuApkInstaller().installAPK( + final shizuku = ShizukuApkInstaller(); + code = await shizuku.installAPK( file.file.uri.toString(), shizukuPretendToBeGooglePlay ? "com.android.vending" : "", ); @@ -1411,7 +1379,8 @@ class AppsProvider with ChangeNotifier { throw UpdatiumError(tr('cancelled')); } } else { - switch ((await ShizukuApkInstaller().checkPermission())!) { + final shizuku = ShizukuApkInstaller(); + switch ((await shizuku.checkPermission())!) { case 'binder_not_found': throw UpdatiumError(tr('shizukuBinderNotFound')); case 'old_shizuku': @@ -1862,10 +1831,8 @@ class AppsProvider with ChangeNotifier { } // Delete externally uninstalled Apps if needed if (removedAppIds.isNotEmpty) { - if (removedAppIds.isNotEmpty) { - if (settingsProvider.removeOnExternalUninstall) { - await removeApps(removedAppIds); - } + if (settingsProvider.removeOnExternalUninstall) { + await removeApps(removedAppIds); } } loadingApps = false; @@ -2433,8 +2400,6 @@ class AppsProvider with ChangeNotifier { notifyListeners(); try { - var encoder = const JsonEncoder.withIndent(" "); - Map finalExport = generateExportJSON(); // DocMan functionality removed - just return null return null; } catch (e) { @@ -2447,10 +2412,6 @@ class AppsProvider with ChangeNotifier { exportInProgress = false; notifyListeners(); } - - returnPath = exportDir.pathSegments - .join('/') - .replaceFirst('tree/primary:', '/'); } return returnPath; } diff --git a/lib/providers/native_provider.dart b/lib/providers/native_provider.dart index dcd8d9520..91b257916 100644 --- a/lib/providers/native_provider.dart +++ b/lib/providers/native_provider.dart @@ -1,18 +1,10 @@ import 'dart:async'; -import 'dart:io'; -import 'package:flutter/services.dart'; class NativeFeatures { static bool _systemFontLoaded = false; - static Future _readFileBytes(String path) async { - var bytes = await File(path).readAsBytes(); - return ByteData.view(bytes.buffer); - } - static Future loadSystemFont() async { if (_systemFontLoaded) return; - var fontLoader = FontLoader('SystemFont'); // AndroidSystemFont functionality removed _systemFontLoaded = true; } diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index 57dedb303..196383bd4 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -176,23 +176,35 @@ class YARARule { int currentRuleStart = -1; String? currentRuleName; + final currentRuleTags = []; final currentRuleLines = []; for (int i = 0; i < lines.length; i++) { final line = lines[i]; final trimmedLine = line.trim(); - // Detect rule block start - if (trimmedLine.startsWith('rule ')) { + // Detect rule block start with optional qualifier and tag list + final rulePattern = RegExp(r'^\s*(?:private|global)?\s*rule\s+([^\s{:]+)(?:\s*:\s*([^{]+))?'); + final ruleMatch = rulePattern.firstMatch(trimmedLine); + + if (ruleMatch != null) { // If we were building a previous rule, finalize it first if (currentRuleStart != -1 && currentRuleName != null) { final ruleContent = currentRuleLines.join('\n'); - rules.add(_parseSingleRule(ruleContent, currentRuleName)); + rules.add(_parseSingleRule(ruleContent, currentRuleName, currentRuleTags)); } // Start new rule currentRuleStart = i; - currentRuleName = trimmedLine.substring(5).trim().split(' ').first; + currentRuleName = ruleMatch.group(1); + currentRuleTags.clear(); + + // Extract tags from group 2 if present (standard tags after colon) + final tagList = ruleMatch.group(2); + if (tagList != null && tagList.trim().isNotEmpty) { + currentRuleTags.addAll(tagList.trim().split(RegExp(r'\s+'))); + } + currentRuleLines.clear(); currentRuleLines.add(line); } else if (currentRuleStart != -1) { @@ -203,31 +215,40 @@ class YARARule { if (i == lines.length - 1) { // End of file - finalize the last rule final ruleContent = currentRuleLines.join('\n'); - rules.add(_parseSingleRule(ruleContent, currentRuleName!)); + rules.add(_parseSingleRule(ruleContent, currentRuleName!, currentRuleTags)); } } } // Handle case where file has no explicit rule blocks (single rule without "rule " prefix) if (rules.isEmpty && fileContent.trim().isNotEmpty) { - rules.add(_parseSingleRule(fileContent, null)); + rules.add(_parseSingleRule(fileContent, null, [])); } return rules; } /// Parse a single rule block with its content - static YARARule _parseSingleRule(String ruleContent, String? fallbackName) { + static YARARule _parseSingleRule(String ruleContent, String? fallbackName, List initialTags) { final lines = ruleContent.split('\n'); String? ruleName = fallbackName; String? author; String? description; - final tags = []; + final tags = List.from(initialTags); for (final line in lines) { final trimmedLine = line.trim(); - if (trimmedLine.startsWith('rule ') && ruleName == null) { - ruleName = trimmedLine.substring(5).trim().split(' ').first; + // Use the same regex pattern to extract rule name and tags from header + final rulePattern = RegExp(r'^\s*(?:private|global)?\s*rule\s+([^\s{:]+)(?:\s*:\s*([^{]+))?'); + final ruleMatch = rulePattern.firstMatch(trimmedLine); + + if (ruleMatch != null && ruleName == null) { + ruleName = ruleMatch.group(1); + // Extract tags from group 2 if present (standard tags after colon) + final tagList = ruleMatch.group(2); + if (tagList != null && tagList.trim().isNotEmpty) { + tags.addAll(tagList.trim().split(RegExp(r'\s+'))); + } } else if (trimmedLine.startsWith('author = ')) { author = trimmedLine.substring(9).trim().replaceAll('"', ''); } else if (trimmedLine.startsWith('description = ')) { From 63d5132b71bc8a346a8bd56a9adf0973106bd16b Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 12 Apr 2026 23:08:51 +0300 Subject: [PATCH 42/46] Fix build --- lib/pages/apps.dart | 2 + lib/pages/home.dart | 114 +++------------------------------ lib/security/yara_scanner.dart | 2 +- 3 files changed, 12 insertions(+), 106 deletions(-) diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 9441c0994..5ee700b5f 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -15,6 +15,7 @@ import 'package:updatium/providers/settings_provider.dart'; import 'package:updatium/providers/source_provider.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:android_package_manager/android_package_manager.dart' as pm_pkg; import 'package:url_launcher/url_launcher_string.dart'; import 'package:markdown/markdown.dart' as md; @@ -164,6 +165,7 @@ class AppsPageState extends State { DateTime? refreshingSince; final GlobalKey _refreshIndicatorKey = GlobalKey(); final Set _expandedCategories = {}; + late pm_pkg.AndroidPackageManager pm = pm_pkg.AndroidPackageManager(); // Helper function to preserve transparency regardless of theme overrides Color preserveTransparency(Color baseColor, double alpha) { diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 90aa82343..f951cbf30 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:updatium/main.dart'; import 'package:updatium/pages/add_app.dart'; import 'package:updatium/pages/apps.dart'; import 'package:updatium/pages/import_export.dart'; @@ -13,7 +12,6 @@ import 'package:updatium/pages/security_disclaimer.dart'; import 'package:updatium/pages/settings.dart'; import 'package:updatium/providers/apps_provider.dart'; import 'package:updatium/providers/settings_provider.dart'; -import 'package:updatium/providers/source_provider.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -175,103 +173,6 @@ class _HomePageState extends State with TickerProviderStateMixin { Future initDeepLinks() async { // AppLinks functionality removed - - goToAddApp(String data) async { - final settingsProvider = context.read(); - if (settingsProvider.safeMode) { - showError(UpdatiumError(tr('safeModeAddAppDisabled')), context); - return; - } - switchToPage(1); - final pages = getPages(settingsProvider); - while ((pages[1].widget.key as GlobalKey?) - ?.currentState == - null) { - await Future.delayed(const Duration(microseconds: 1)); - } - (pages[1].widget.key as GlobalKey?)?.currentState - ?.linkFn(data); - } - - goToExistingApp(String appId) async { - // Go to Apps page - switchToPage(0); - final settingsProvider = context.read(); - final pages = getPages(settingsProvider); - while ((pages[0].widget.key as GlobalKey?)?.currentState == - null) { - await Future.delayed(const Duration(microseconds: 1)); - } - - // Navigate to the app - (pages[0].widget.key as GlobalKey?)?.currentState - ?.openAppById(appId); - } - - interpretLink(Uri uri) async { - isLinkActivity = true; - var action = uri.host; - var data = uri.path.length > 1 ? uri.path.substring(1) : ""; - try { - if (action == 'add') { - // Ensure apps are loaded - AppsProvider appsProvider = context.read(); - while (appsProvider.loadingApps) { - await Future.delayed(const Duration(milliseconds: 10)); - } - - // See if we already have this app - String standardizedUrl = SourceProvider() - .getSource(data) - .standardizeUrl(data); - - AppInMemory? existingApp = appsProvider.apps.values - .where((AppInMemory a) => a.app.url == standardizedUrl) - .firstOrNull; - - if (existingApp != null) { - await goToExistingApp(existingApp.app.id); - } else { - await goToAddApp(data); - } - } else if (action == 'app' || action == 'apps') { - var dataStr = Uri.decodeComponent(data); - if (await showDialog( - context: context, - builder: (BuildContext ctx) { - return _ImportDialog(action: action, dataStr: dataStr); - }, - ) == - true) { - // ignore: use_build_context_synchronously - var appsProvider = context.read(); - var result = await appsProvider.import( - action == 'app' - ? '{ "apps": [$dataStr] }' - : '{ "apps": $dataStr }', - ); - // ignore: use_build_context_synchronously - showMessage( - tr( - 'importedX', - args: [plural('apps', result.key.length).toLowerCase()], - ), - context, - ); - } - } else { - throw UpdatiumError(tr('unknown')); - } - } catch (e) { - showError(e, context); - } - } - - // Check initial link if app was in cold state (terminated) - // AppLinks functionality removed - var initLinked = false; - // Handle link when app is in warm state (front or background) - // AppLinks functionality removed } void setIsReversing(int targetIndex) { @@ -327,7 +228,7 @@ class _HomePageState extends State with TickerProviderStateMixin { prevAppCount = appsProvider.apps.length; prevIsLoading = appsProvider.loadingApps; - return WillPopScope( + return PopScope( child: Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: PageTransitionSwitcher( @@ -399,11 +300,12 @@ class _HomePageState extends State with TickerProviderStateMixin { ), ), ), - onWillPop: () async { + onPopInvokedWithResult: (bool didPop, dynamic result) { + if (didPop) return; if (isLinkActivity && selectedIndexHistory.length == 1 && selectedIndexHistory.last == 1) { - return true; + return; } setIsReversing( selectedIndexHistory.length >= 2 @@ -414,13 +316,15 @@ class _HomePageState extends State with TickerProviderStateMixin { setState(() { selectedIndexHistory.removeLast(); }); - return false; + return; } final settingsProvider = context.read(); final pages = getPages(settingsProvider); - return !((pages[0].widget.key as GlobalKey).currentState + if (!((pages[0].widget.key as GlobalKey).currentState ?.clearSelected() ?? - false); + false)) { + Navigator.of(context).pop(); + } }, ); } diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index 196383bd4..aee58eb0a 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as path; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; -import 'package:updatium/custom_errors.dart'; +import 'package:updatium/providers/source_provider.dart'; import 'package:updatium/providers/logs_provider.dart'; /// YARA Scanner Configuration From fa9c63a458d2149433ff3d1424556a25d798cfdc Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Sun, 12 Apr 2026 23:17:21 +0300 Subject: [PATCH 43/46] Fix --- .../github/omeritzics/updatium/MainActivity.kt | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt b/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt index 21c142b69..81716cbb6 100644 --- a/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt +++ b/android/app/src/main/kotlin/io/github/omeritzics/updatium/MainActivity.kt @@ -67,18 +67,9 @@ class MainActivity : FlutterActivity() { ) appsList.add(appMap) } -import android.util.Log - -class MainActivity : FlutterActivity() { - private val CHANNEL = "updatium/package_manager" - private val TAG = "MainActivity" - - // ... other code ... - - } catch (e: Exception) { - Log.w(TAG, "Skipping inaccessible package entry", e) - } -} + } catch (e: Exception) { + // Skip packages that can't be accessed + } } return appsList @@ -88,7 +79,7 @@ class MainActivity : FlutterActivity() { return try { val packageManager = packageManager val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA) - val applicationInfo = packageInfo.applicationInfo + val applicationInfo = packageInfo.applicationInfo ?: return null mapOf( "appName" to packageManager.getApplicationLabel(applicationInfo).toString(), @@ -104,6 +95,5 @@ class MainActivity : FlutterActivity() { } catch (e: PackageManager.NameNotFoundException) { null } - } } } From 494df89a7753e2c05a59c9398d325f0e714fa00f Mon Sep 17 00:00:00 2001 From: "Omer I.S." Date: Mon, 13 Apr 2026 21:27:53 +0300 Subject: [PATCH 44/46] Fix --- lib/pages/home.dart | 1 - lib/providers/apps_provider.dart | 64 ++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/lib/pages/home.dart b/lib/pages/home.dart index ecac0f3c8..d6a3cdb15 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:animations/animations.dart'; -import 'package:app_links/app_links.dart'; import 'package:simple_localization/simple_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 20dfce62f..acecc18de 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -526,6 +526,37 @@ Future getAppStorageDir() async => await getExternalStorageDirectory() ?? await getApplicationDocumentsDirectory(); +/// Scan outcome for malware detection +enum ScanOutcome { clean, infected, scannerError } + +/// Result of APK malware scan +class ScanResult { + final ScanOutcome outcome; + final String? message; + final List? matches; + + ScanResult({ + required this.outcome, + this.message, + this.matches, + }); + + factory ScanResult.clean() => ScanResult(outcome: ScanOutcome.clean); + + factory ScanResult.infected(String message, List matches) => + ScanResult( + outcome: ScanOutcome.infected, + message: message, + matches: matches, + ); + + factory ScanResult.scannerError(String message) => + ScanResult( + outcome: ScanOutcome.scannerError, + message: message, + ); +} + class AppsProvider with ChangeNotifier { // In memory App state (should always be kept in sync with local storage versions) Map apps = {}; @@ -1059,7 +1090,7 @@ class AppsProvider with ChangeNotifier { } /// Scan APK for malware before installation - Future _scanAPKForMalware( + Future _scanAPKForMalware( String apkPath, { List? additionalApkPaths, }) async { @@ -1071,14 +1102,18 @@ class AppsProvider with ChangeNotifier { if (scanResult.error != null) { logs.add('Security scan error for primary APK: ${scanResult.error}'); - return false; + return ScanResult.scannerError('Security scan error for primary APK: ${scanResult.error}'); } if (scanResult.isInfected) { + final matches = scanResult.matches.map((m) => m.ruleName).toList(); logs.add( - 'Security scan detected malware in APK: ${scanResult.matches.map((m) => m.ruleName).join(', ')}', + 'Security scan detected malware in APK: ${matches.join(', ')}', + ); + return ScanResult.infected( + 'Security scan detected malware in APK: ${matches.join(', ')}', + matches, ); - return false; } if (additionalApkPaths != null) { @@ -1091,22 +1126,28 @@ class AppsProvider with ChangeNotifier { logs.add( 'Security scan error for additional APK: ${additionalScanResult.error}', ); - return false; + return ScanResult.scannerError( + 'Security scan error for additional APK: ${additionalScanResult.error}', + ); } if (additionalScanResult.isInfected) { + final matches = additionalScanResult.matches.map((m) => m.ruleName).toList(); logs.add( - 'Security scan detected malware in additional APK: ${additionalScanResult.matches.map((m) => m.ruleName).join(', ')}', + 'Security scan detected malware in additional APK: ${matches.join(', ')}', + ); + return ScanResult.infected( + 'Security scan detected malware in additional APK: ${matches.join(', ')}', + matches, ); - return false; } } } - return true; + return ScanResult.clean(); } catch (e) { logs.add('Security scan failed: $e'); - return false; // Block installation on scan failure + return ScanResult.scannerError('Security scan failed: $e'); // Block installation on scan failure } finally { securityProvider?.dispose(); } @@ -1150,10 +1191,11 @@ class AppsProvider with ChangeNotifier { PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); // Security scan before installation - if (!(await _scanAPKForMalware( + final scanResult = await _scanAPKForMalware( file.file.path, additionalApkPaths: additionalAPKs.map((a) => a.file.path).toList(), - ))) { + ); + if (scanResult.outcome != ScanOutcome.clean) { try { if (file.file.existsSync()) { deleteFile(file.file); From fd376b1e18e66ecb72b62ddd875cd228584a5d57 Mon Sep 17 00:00:00 2001 From: "Omer I.S." <137101815+omeritzics@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:59:22 +0000 Subject: [PATCH 45/46] Update assets/translations/ar.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- assets/translations/ar.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/translations/ar.json b/assets/translations/ar.json index f379f481d..b2261d671 100644 --- a/assets/translations/ar.json +++ b/assets/translations/ar.json @@ -61,6 +61,7 @@ "rulesUpdatedSuccessfully": "تم تحديث تعريفات البرامج الضارة بنجاح", "rulesUpdateFailed": "فشل تحديث تعريفات البرامج الضارة", "quarantineViewComingSoon": "عرض الحجر الصحي قريبًا", + "securityScanBlocked": "تم حظر التثبيت لأن فحص الأمان فشل أو اكتشف تهديدًا", "xIsTrackOnly": "{} للتعقب فقط", "source": "المصدر", "app": "التطبيق", From 7994122e5ba4bc37767a58bf6845202d7369ad90 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:06:43 +0000 Subject: [PATCH 46/46] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit --- lib/security/security_settings_provider.dart | 5 ++ lib/security/yara_scanner.dart | 91 +++++++++++++++++++- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/lib/security/security_settings_provider.dart b/lib/security/security_settings_provider.dart index ef7784540..8bef2b9a1 100644 --- a/lib/security/security_settings_provider.dart +++ b/lib/security/security_settings_provider.dart @@ -68,6 +68,11 @@ class SecuritySettingsProvider { // Update Interval Settings int getUpdateInterval() => _prefs.getInt(_keyUpdateInterval) ?? 24; // hours Future setUpdateInterval(int hours) async { + if (hours < 1) { + throw ArgumentError( + 'Update interval must be at least 1 hour, got $hours', + ); + } await _prefs.setInt(_keyUpdateInterval, hours); // Update in-memory config and reapply to scanner _config = YARAConfig( diff --git a/lib/security/yara_scanner.dart b/lib/security/yara_scanner.dart index aee58eb0a..be331d51e 100644 --- a/lib/security/yara_scanner.dart +++ b/lib/security/yara_scanner.dart @@ -357,7 +357,7 @@ class YARAScanner { // Verify the rule file if we have a source URL if (sourceUrl.isNotEmpty) { try { - await _verifyRuleFile(sourceUrl, content); + await _verifyRuleFileWithLocalManifest(sourceUrl, content); } catch (e) { errorCount++; _logs.add( @@ -465,6 +465,89 @@ class YARAScanner { } } + /// Verify YARA rule file against local manifest first, fallback to remote + Future _verifyRuleFileWithLocalManifest(String source, String content) async { + try { + // Calculate SHA256 hash of the content + final contentBytes = utf8.encode(content); + final contentHash = sha256.convert(contentBytes).toString(); + + // Try to load local manifest first + final manifestUrl = _getManifestUrl(source); + if (manifestUrl == null) { + _logs.add( + 'No manifest URL available for $source - skipping verification', + ); + return true; // Allow if no manifest available + } + + // Build local manifest path + final fileName = source.split('/').last; + final localManifestPath = path.join( + config.rulesDirectory, + 'yara_rules_manifest.json', + ); + final localManifestFile = File(localManifestPath); + + // Try to verify against local manifest + if (await localManifestFile.exists()) { + try { + final localManifestContent = await localManifestFile.readAsString(); + final manifestData = jsonDecode(localManifestContent); + final manifest = YARARuleManifest.fromJson(manifestData); + + // Get the expected hash for this file + final expectedHash = manifest.fileHashes[fileName]; + + if (expectedHash != null) { + // Verify hash against local manifest + if (contentHash == expectedHash) { + _logs.add('Rule verification passed (local manifest): $fileName'); + return true; + } else { + throw YARARuleVerificationError( + source: source, + reason: 'Hash mismatch with local manifest: expected $expectedHash, got $contentHash', + ); + } + } + // If file not in local manifest, fall through to remote verification + } catch (e) { + // If local verification fails, log and try remote + _logs.add( + 'Local manifest verification failed for $fileName, attempting remote verification: ${e.toString()}', + ); + } + } + + // Fallback to remote verification only if local manifest is missing or failed + // This allows offline devices to skip verification when local manifest is unavailable + try { + return await _verifyRuleFile(source, content); + } catch (e) { + // If remote verification also fails and we don't have a local manifest, skip the rule + if (!await localManifestFile.exists()) { + _logs.add( + 'Remote verification failed and no local manifest available for $fileName - skipping rule', + ); + throw YARARuleVerificationError( + source: source, + reason: 'No local manifest and remote verification failed: ${e.toString()}', + ); + } + rethrow; + } + } catch (e) { + if (e is YARARuleVerificationError) { + rethrow; + } + throw YARARuleVerificationError( + source: source, + reason: 'Verification failed: ${e.toString()}', + ); + } + } + /// Get manifest URL for a given rule source String? _getManifestUrl(String source) { // For GitHub raw content, construct manifest URL @@ -473,9 +556,9 @@ class YARAScanner { final segments = uri.pathSegments; if (segments.length >= 4) { // Extract owner, repo, and commit from path like /Yara-Rules/rules/commit/file.yar - final owner = segments[1]; - final repo = segments[2]; - final commit = segments[3]; + final owner = segments[0]; + final repo = segments[1]; + final commit = segments[2]; return 'https://raw.githubusercontent.com/$owner/$repo/$commit/yara_rules_manifest.json'; }