downloadFile method

Future<String?> downloadFile({
  1. required String downloadFileUrl,
  2. void onProgress(
    1. double progress
    )?,
  3. @Deprecated('Use a try-catch block to handle FileDownloadException instead.') void onError(
    1. String error
    )?,
  4. void onTimeLeft(
    1. String error
    )?,
  5. void onSpeed(
    1. String error
    )?,
  6. void onTotalSize(
    1. String error
    )?,
  7. void onDownloadedSize(
    1. String error
    )?,
  8. String? downloadFileName,
  9. bool deleteOnError = false,
})

Downloads a file from the provided URL to the device's external storage.

Prevents concurrent downloads by throwing a FileDownloadException with DownloadErrorType.alreadyRunning if a download is currently active.

The file is saved in the external storage directory with the name specified by downloadFileName (defaults to "downloadApk.apk").

Parameters:

  • downloadFileUrl: The direct URL to the APK file.
  • onProgress: Callback returning the download progress as a double (0.0 to 1.0).
  • onError: Callback for legacy error handling.
  • onTimeLeft: Callback returning a formatted string of the estimated time remaining.
  • onSpeed: Callback returning a formatted string of the current download speed.
  • onTotalSize: Callback returning a formatted string of the total file size.
  • onDownloadedSize: Callback returning a formatted string of the bytes downloaded so far.
  • downloadFileName: Optional custom name for the downloaded file (without the .apk extension).
  • deleteOnError: If true, automatically deletes the partial file if the download fails due to a network or server error. Defaults to false.

Returns the absolute path to the downloaded file on success, or null if handled via legacy error.

Implementation

Future<String?> downloadFile({
  required String downloadFileUrl,
  void Function(double progress)? onProgress,
  @Deprecated(
    'Use a try-catch block to handle FileDownloadException instead.',
  )
  void Function(String error)? onError,
  void Function(String timeLeft)? onTimeLeft,
  void Function(String speed)? onSpeed,
  void Function(String totalSize)? onTotalSize,
  void Function(String downloadedSize)? onDownloadedSize,
  String? downloadFileName,
  bool deleteOnError = false,
}) async {
  if (_isDownloading) {
    final exception = FileDownloadException(
      type: DownloadErrorType.alreadyRunning,
      originalError:
          "A download task is currently active. Please wait for it to finish or cancel it.",
    );

    return _handleErrorBridge(
      exception,
      "Download already in progress",
      onError,
    );
  }

  _isDownloading = true;
  _cancelToken = CancelToken();

  int lastBytes = 0;
  DateTime lastUpdateTime = DateTime.now();

  try {
    Directory? directory = await getExternalStorageDirectory();

    if (directory == null) {
      final exception = FileDownloadException(
        type: DownloadErrorType.storageAccessDenied,
        originalError: "Unable to access storage directory",
      );
      return _handleErrorBridge(
        exception,
        "Unable to access storage directory",
        onError,
      );
    }

    String savePath =
        "${directory.path}/${downloadFileName ?? "downloadApk"}.apk";

    _downloadPath = savePath;

    Response response = await _dio.download(
      downloadFileUrl,
      savePath,
      cancelToken: _cancelToken,
      onReceiveProgress: (count, total) {
        if (total > 0) {
          if (onProgress != null) {
            onProgress(count / total);
          }

          if (onTotalSize != null) {
            onTotalSize(_formatBytes(total));
          }

          if (onDownloadedSize != null) {
            onDownloadedSize(_formatBytes(count));
          }

          DateTime now = DateTime.now();
          Duration interval = now.difference(lastUpdateTime);

          if (interval.inMilliseconds >= 500 || count == total) {
            int bytesSinceLast = count - lastBytes;
            double secondsSinceLast = interval.inMilliseconds / 1000.0;

            if (secondsSinceLast > 0) {
              double speedBps = bytesSinceLast / secondsSinceLast;

              if (onSpeed != null) {
                onSpeed(_formatSpeed(speedBps));
              }

              if (onTimeLeft != null && speedBps > 0) {
                double remainingBytes = (total - count).toDouble();
                double remainingSeconds = remainingBytes / speedBps;
                onTimeLeft(_formatTimeLeft(remainingSeconds));
              }
            }

            lastBytes = count;
            lastUpdateTime = now;
          }
        }
      },
    );

    if (response.statusCode == 200) {
      _downloadPath = null;
      return savePath;
    } else {
      // Add the cleanup here too!
      if (deleteOnError && _downloadPath != null) {
        File file = File(_downloadPath!);
        if (await file.exists()) {
          try {
            await file.delete();
            log(
              "Partial file deleted automatically due to bad response code.",
            );
          } catch (_) {}
        }
        _downloadPath = null;
      }
      final exception = FileDownloadException(
        type: DownloadErrorType.badResponse,
        statusCode: response.statusCode,
        originalError: "Server returned status: ${response.statusCode}",
      );
      return _handleErrorBridge(
        exception,
        "Unable to download the file",
        onError,
      );
    }
  } on DioException catch (e, sc) {
    log("DioException in downloadFile", error: e, stackTrace: sc);

    if (deleteOnError &&
        e.type != DioExceptionType.cancel &&
        _downloadPath != null) {
      File file = File(_downloadPath!);
      if (await file.exists()) {
        try {
          await file.delete();
          log("Partial file deleted automatically due to DioException.");
        } catch (_) {}
      }
      _downloadPath = null; // Clear state after deleting
    }

    final exception = FileDownloadException(
      type: _mapDioErrorToEnum(e.type),
      statusCode: e.response?.statusCode,
      originalError: e,
    );
    String legacyErrorString = _getLegacyErrorMessage(e);
    return _handleErrorBridge(exception, legacyErrorString, onError);
  } catch (e, sc) {
    log("Unknown Exception in downloadFile", error: e, stackTrace: sc);
    // 3. Add Auto-Cleanup for unknown errors too
    if (deleteOnError && _downloadPath != null) {
      File file = File(_downloadPath!);
      if (await file.exists()) {
        try {
          await file.delete();
          log("Partial file deleted automatically due to Unknown Exception.");
        } catch (_) {}
      }
      _downloadPath = null; // Clear state after deleting
    }
    final exception = FileDownloadException(
      type: DownloadErrorType.unknown,
      originalError: e,
    );
    return _handleErrorBridge(
      exception,
      "Error Occurred while downloading the file",
      onError,
    );
  } finally {
    _isDownloading = false;
  }
}