startDownload method

Future<void> startDownload()

开始或恢复下载

断点续传策略:

  1. 文件名包含 version+buildVersion,版本变化自动重新下载
  2. 检测本地文件大小,若超出预期则删除重来
  3. 服务端不支持 Range(返回200而非206)时,自动清理本地文件从头写入
  4. 大小不匹配或 HTTP 416 时自动重试

Implementation

Future<void> startDownload() async {
  _ensureAndroid();
  _ensureInitialized();

  if (_updateInfo?.latestVersion == null) {
    statusNotifier.value = DownloadStatus.error;
    final error = StateError('No update is available to download.');
    _emit(UpgraderEventType.downloadError, error.message);
    _safeErrorCallback(error);
    return;
  }
  if (statusNotifier.value == DownloadStatus.downloading) {
    _emit(UpgraderEventType.log, '下载已在进行中,跳过重复请求');
    return;
  }

  final session = ++_sessionSequence;
  _activeDownloadSession = session;
  statusNotifier.value = DownloadStatus.downloading;
  progressNotifier.value = 0.0;

  final latest = _updateInfo!.latestVersion!;
  try {
    final tempDir = await getTemporaryDirectory();
    if (!_isActiveSession(session)) return;

    final fileName = _buildSafeApkFileName(latest);
    _savePath = '${tempDir.path}${Platform.pathSeparator}$fileName';
    await _cleanOldApkFiles(tempDir, _savePath!);
    if (!_isActiveSession(session)) return;

    final file = File(_savePath!);
    final existingLength = await file.exists() ? await file.length() : 0;
    if (latest.apkSize > 0 && existingLength < latest.apkSize) {
      final availableBytes = await TinyUpgraderPlatform.instance
          .getAvailableStorageBytes(tempDir.path);
      final requiredBytes =
          (latest.apkSize - existingLength) +
          latest.apkSize +
          _minFreeSpaceMarginBytes;
      if (availableBytes < requiredBytes) {
        final error = InsufficientStorageException(
          availableBytes: availableBytes,
          requiredBytes: requiredBytes,
        );
        statusNotifier.value = DownloadStatus.error;
        _emit(UpgraderEventType.downloadError, '存储空间不足,无法下载更新', {
          'sessionId': session,
          'availableBytes': availableBytes,
          'requiredBytes': requiredBytes,
        });
        _safeErrorCallback(error);
        return;
      }
    }

    final headers = <String, String>{};
    final downloadUrl = _resolveDownloadUrl(latest.downloadUrl);
    if (_ossConfig != null && !_ossConfig!.isPublicRead) {
      headers.addAll(
        OssSigner.generateHeaders(
          config: _ossConfig!,
          downloadUrl: downloadUrl,
        ),
      );
    }

    _emit(UpgraderEventType.downloadStart, '启动 Android 前台下载服务', {
      'sessionId': session,
      'fileName': fileName,
      'downloadUrl': _redactUrl(downloadUrl),
      'apkSize': latest.apkSize,
      'version': latest.version,
      'buildVersion': latest.buildVersion,
    });
    await TinyUpgraderPlatform.instance.startForegroundDownload(
      ForegroundDownloadRequest(
        sessionId: session,
        url: downloadUrl,
        savePath: _savePath!,
        headers: headers,
        expectedSize: latest.apkSize,
        expectedHash: latest.apkHashCode,
        hashAlgorithm: latest.apkHashAlgorithm.name,
        maxNetworkRetryCount: _maxNetworkRetryCount,
        maxValidationRetryCount: _maxValidationRetryCount,
        minFreeSpaceMarginBytes: _minFreeSpaceMarginBytes,
      ),
    );
  } catch (error) {
    if (!_isActiveSession(session)) return;
    statusNotifier.value = DownloadStatus.error;
    _emit(UpgraderEventType.downloadError, '无法启动 Android 前台下载服务', {
      'sessionId': session,
      'error': error.toString(),
    });
    _safeErrorCallback(error);
  }
}