downloadAndInstall method

Future<bool> downloadAndInstall(
  1. PatchInfo patch
)

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;
  }
}