downloadAndInstall method
Download and install patch via platform channel
Downloads the patch, verifies integrity, and installs to the engine. App restart is required to load the patched code.
Returns true if patch was installed successfully.
Implementation
Future<bool> downloadAndInstall(PatchInfo patch) async {
_ensureInitialized();
try {
if (_config.enableDebugLogging) {
print('[QuicUI] Starting patch download and install process');
print('[QuicUI] Patch version: ${patch.version}');
print('[QuicUI] Patch size: ${patch.size} bytes');
}
// 1. Get device architecture
final architecture = await CodePushMethodChannel.getDeviceArchitecture();
if (_config.enableDebugLogging) {
print('[QuicUI] Device architecture: $architecture');
}
// 2. Download patch to temporary directory
final tempDir = Directory.systemTemp;
final compressedFile = File('${tempDir.path}/quicui_patch_${patch.version}.compressed');
// Determine file extension based on platform
// iOS uses .vmcode (interpreter approach)
// Android uses .quicui (binary patch approach)
String fileExtension = 'quicui'; // default for Android
if (patch.platform == 'ios') {
fileExtension = 'vmcode';
} else if (patch.platform == 'android') {
fileExtension = 'quicui';
}
final patchFile = File('${tempDir.path}/quicui_patch_${patch.version}.$fileExtension');
final client = http.Client();
String? compressionFormat;
try {
final response = await client.get(
Uri.parse(patch.downloadUrl),
headers: {
'Authorization': 'Bearer $_apiKey',
'apikey': _apiKey,
},
);
if (response.statusCode != 200) {
if (_config.enableDebugLogging) {
print('[QuicUI] ❌ Failed to download patch: ${response.statusCode}');
}
_config.onError?.call('Failed to download patch: ${response.statusCode}');
return false;
}
// Check if patch is compressed - first check URL query parameter
final uri = Uri.parse(patch.downloadUrl);
compressionFormat = uri.queryParameters['compression'];
// Fallback to response headers if not in URL
compressionFormat ??= response.headers['content-encoding'] ??
response.headers['x-compression-format'];
if (_config.enableDebugLogging) {
print('[QuicUI] Patch downloaded: ${response.bodyBytes.length} bytes');
print('[QuicUI] Compression format: ${compressionFormat ?? "none"}');
}
// Write compressed data first
await compressedFile.writeAsBytes(response.bodyBytes);
// Decompress if needed
if (compressionFormat != null && compressionFormat.isNotEmpty) {
try {
final compressedBytes = await compressedFile.readAsBytes();
List<int>? decompressedBytes;
if (compressionFormat == 'xz') {
decompressedBytes = XZDecoder().decodeBytes(compressedBytes);
} else if (compressionFormat == 'gz' || compressionFormat == 'gzip') {
decompressedBytes = GZipDecoder().decodeBytes(compressedBytes);
} else if (compressionFormat == 'bz2' || compressionFormat == 'bzip2') {
decompressedBytes = BZip2Decoder().decodeBytes(compressedBytes);
}
if (decompressedBytes != null) {
await patchFile.writeAsBytes(decompressedBytes);
await compressedFile.delete(); // Clean up compressed file
if (_config.enableDebugLogging) {
print('[QuicUI] ✅ Decompression successful');
}
} else {
await compressedFile.rename(patchFile.path);
}
} catch (e) {
_config.onError?.call('Decompression error: $e');
// Try to use compressed file as-is
try {
await compressedFile.rename(patchFile.path);
} catch (renameError) {
return false;
}
}
} else {
await compressedFile.rename(patchFile.path);
}
} finally {
client.close();
}
// 3. Verify hash
if (patch.signature.isNotEmpty) {
final fileBytes = await patchFile.readAsBytes();
final calculatedHash = sha256.convert(fileBytes).toString();
if (_config.enableDebugLogging) {
print('[QuicUI] Patch hash: $calculatedHash');
}
}
// 4. Verify signature (if public key configured)
if (_config.publicKey != null && patch.signature.isNotEmpty) {
final fileBytes = await patchFile.readAsBytes();
final isValid = await _verifier.verify(
data: fileBytes,
signature: patch.signature,
);
if (!isValid) {
_config.onError?.call('Patch signature verification failed');
await patchFile.delete();
return false;
}
if (_config.enableDebugLogging) {
print('[QuicUI] Signature verification passed');
}
}
// 5. Calculate hash for engine validation
final fileBytes = await patchFile.readAsBytes();
final patchHash = sha256.convert(fileBytes).toString();
// 6. Transfer to native engine via platform channel
final success = await CodePushMethodChannel.installPatch(
patchPath: patchFile.path,
patchId: patch.patchId,
version: patch.version,
hash: patchHash,
architecture: architecture,
signature: patch.signature,
);
if (!success) {
_config.onError?.call('Failed to install patch via platform channel');
await patchFile.delete();
return false;
}
if (_config.enableDebugLogging) {
print('[QuicUI] ✅ Patch successfully installed to code cache');
print('[QuicUI] 🔄 App restart required to load patched code');
}
// 7. Cleanup temp file
try {
await patchFile.delete();
} catch (e) {
// Ignore cleanup errors
}
// 8. Notify success
if (_config.enableDebugLogging) {
print('[QuicUI] Patch installation complete!');
}
return true;
} catch (e) {
_config.onError?.call('Error in downloadAndInstall: $e');
return false;
}
}