Files
SpotiFLAC-Mobile/lib/services/update_checker.dart
T
zarzet 08bca30fcd perf: optimize state management, add HTTPS validation, improve UI performance
- Add HTTPS-only validation for APK downloads and update checks
- Use .select() for Riverpod providers to prevent unnecessary rebuilds
- Add keys to all list builders for efficient updates
- Implement request cancellation for outdated API requests
- Debounce all network requests (URLs and searches)
- Limit file existence cache to 500 entries
- Add ref.onDispose for timer cleanup
- Add error handling for share intent stream
- Redesign About page with Material Expressive 3 style
- Rename Search tab to Home
- Remove Features section from README
2026-01-03 00:46:34 +07:00

169 lines
5.2 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:spotiflac_android/constants/app_info.dart';
import 'package:spotiflac_android/utils/logger.dart';
final _log = AppLogger('UpdateChecker');
class UpdateInfo {
final String version;
final String changelog;
final String downloadUrl;
final String? apkDownloadUrl;
final DateTime publishedAt;
const UpdateInfo({
required this.version,
required this.changelog,
required this.downloadUrl,
this.apkDownloadUrl,
required this.publishedAt,
});
}
class UpdateChecker {
static const String _apiUrl = 'https://api.github.com/repos/${AppInfo.githubRepo}/releases/latest';
static Future<String> _getDeviceArch() async {
if (!Platform.isAndroid) return 'unknown';
try {
final cpuInfo = await File('/proc/cpuinfo').readAsString();
if (cpuInfo.contains('AArch64') || cpuInfo.contains('aarch64')) {
return 'arm64';
}
final result = await Process.run('uname', ['-m']);
final arch = result.stdout.toString().trim().toLowerCase();
if (arch.contains('aarch64') || arch.contains('arm64')) {
return 'arm64';
} else if (arch.contains('armv7') || arch.contains('arm')) {
return 'arm32';
} else if (arch.contains('x86_64')) {
return 'x86_64';
} else if (arch.contains('x86') || arch.contains('i686')) {
return 'x86';
}
return 'arm64';
} catch (e) {
_log.e('Error detecting arch: $e');
return 'arm64';
}
}
static Future<UpdateInfo?> checkForUpdate() async {
try {
final response = await http.get(
Uri.parse(_apiUrl),
headers: {'Accept': 'application/vnd.github.v3+json'},
).timeout(const Duration(seconds: 10));
if (response.statusCode != 200) {
_log.w('GitHub API returned ${response.statusCode}');
return null;
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
final tagName = data['tag_name'] as String? ?? '';
final latestVersion = tagName.replaceFirst('v', '');
if (!_isNewerVersion(latestVersion, AppInfo.version)) {
_log.i('No update available (current: ${AppInfo.version}, latest: $latestVersion)');
return null;
}
final body = data['body'] as String? ?? 'No changelog available';
final htmlUrl = data['html_url'] as String? ?? '${AppInfo.githubUrl}/releases';
final publishedAt = DateTime.tryParse(data['published_at'] as String? ?? '') ?? DateTime.now();
final deviceArch = await _getDeviceArch();
_log.d('Device architecture: $deviceArch');
String? arm64Url;
String? arm32Url;
String? universalUrl;
final assets = data['assets'] as List<dynamic>? ?? [];
for (final asset in assets) {
final name = (asset['name'] as String? ?? '').toLowerCase();
if (name.endsWith('.apk')) {
final downloadUrl = asset['browser_download_url'] as String?;
// Only accept HTTPS URLs for security
final uri = downloadUrl != null ? Uri.tryParse(downloadUrl) : null;
if (uri == null || uri.scheme != 'https') {
_log.w('Skipping non-HTTPS APK URL: $downloadUrl');
continue;
}
if (name.contains('arm64') || name.contains('v8a')) {
arm64Url = downloadUrl;
} else if (name.contains('arm32') || name.contains('v7a') || name.contains('armeabi')) {
arm32Url = downloadUrl;
} else if (name.contains('universal')) {
universalUrl = downloadUrl;
}
}
}
String? apkUrl;
if (deviceArch == 'arm64') {
apkUrl = arm64Url ?? universalUrl ?? arm32Url;
} else if (deviceArch == 'arm32') {
apkUrl = arm32Url ?? universalUrl;
} else {
apkUrl = universalUrl ?? arm64Url ?? arm32Url;
}
_log.i('Update available: $latestVersion, APK URL: $apkUrl');
return UpdateInfo(
version: latestVersion,
changelog: body,
downloadUrl: htmlUrl,
apkDownloadUrl: apkUrl,
publishedAt: publishedAt,
);
} catch (e) {
_log.e('Error checking for updates: $e');
return null;
}
}
static bool _isNewerVersion(String latest, String current) {
try {
final latestBase = latest.split('-').first;
final currentBase = current.split('-').first;
final latestParts = latestBase.split('.').map(int.parse).toList();
final currentParts = currentBase.split('.').map(int.parse).toList();
while (latestParts.length < 3) {
latestParts.add(0);
}
while (currentParts.length < 3) {
currentParts.add(0);
}
for (int i = 0; i < 3; i++) {
if (latestParts[i] > currentParts[i]) return true;
if (latestParts[i] < currentParts[i]) return false;
}
final latestHasSuffix = latest.contains('-');
final currentHasSuffix = current.contains('-');
if (!latestHasSuffix && currentHasSuffix) return true;
return false;
} catch (e) {
_log.e('Error comparing versions: $e');
return false;
}
}
static String get currentVersion => AppInfo.version;
}