private PreparedDownloadContext prepareDownload(final GetObjectRequest getObjectRequest,
                                                    final File file, final TransferStateChangeListener stateListener,
                                                    final S3ProgressListener s3progressListener,
                                                    final boolean resumeExistingDownload,
                                                    final long timeoutMillis,
                                                    final Integer lastFullyDownloadedPart,
                                                    final long lastModifiedTimeRecordedDuringPause,
                                                    final boolean resumeOnRetry,
                                                    final Long lastFullyDownloadedPartPosition,
                                                    final String lastEtagDuringPause) {
        assertParameterNotNull(getObjectRequest,
                "A valid GetObjectRequest must be provided to initiate download");
        assertParameterNotNull(file,
                "A valid file must be provided to download into");

        appendSingleObjectUserAgent(getObjectRequest);
        String description = "Downloading from " + getObjectRequest.getBucketName() + "/" + getObjectRequest.getKey();

        TransferProgress transferProgress = new TransferProgress();
        // S3 progress listener to capture the persistable transfer when available
        S3ProgressListenerChain listenerChain = new S3ProgressListenerChain(
                // The listener for updating transfer progress
                new TransferProgressUpdatingListener(transferProgress),
                getObjectRequest.getGeneralProgressListener(),
                s3progressListener); // Listeners included in the original request
        // The listener chain used by the low-level GetObject request.
        // This listener chain ignores any COMPLETE event, so that we could
        // delay firing the signal until the high-level download fully finishes.
        getObjectRequest
                .setGeneralProgressListener(new ProgressListenerChain(new TransferCompletionFilter(), listenerChain));

        GetObjectMetadataRequest getObjectMetadataRequest = RequestCopyUtils.createGetObjectMetadataRequestFrom(getObjectRequest);
        final ObjectMetadata objectMetadata = s3.getObjectMetadata(getObjectMetadataRequest);

        // Used to check if the object is modified between pause and resume
        long lastModifiedTime = objectMetadata.getLastModified().getTime();
        final String etag = objectMetadata.getETag();
        if (getObjectRequest.getMatchingETagConstraints() != null
                && getObjectRequest.getMatchingETagConstraints().isEmpty()) {
            if (etag != null) {
                if (log.isDebugEnabled()) {
                    log.debug("Setting matching ETag constraint: " + etag);
                }
                getObjectRequest.withMatchingETagConstraint(etag);
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("ETag constraint not set - value missing in metadata");
                }
            }
        }

        long startingByte = 0;
        long lastByte;

        long[] range = getObjectRequest.getRange();
        if (range != null && range.length == 2) {
            startingByte = range[0];
            lastByte = range[1];
        } else {
            lastByte = objectMetadata.getContentLength() - 1;
        }

        final long origStartingByte = startingByte;
        final boolean isDownloadParallel = !configuration.isDisableParallelDownloads()
                && TransferManagerUtils.isDownloadParallelizable(s3, getObjectRequest, ServiceUtils.getPartCount(getObjectRequest, s3));

        // We still pass the unfiltered listener chain into DownloadImpl
        final DownloadImpl download = new DownloadImpl(description, transferProgress, listenerChain, null,
                stateListener, getObjectRequest, file, objectMetadata, isDownloadParallel);

        long totalBytesToDownload = lastByte - startingByte + 1;
        transferProgress.setTotalBytesToTransfer(totalBytesToDownload);

        // Range information is needed for auto retry of downloads so a retry
        // request can start at the last downloaded location in the range.
        //
        // For obvious reasons, setting a Range header only makes sense if the
        // object actually has content because it's inclusive, otherwise S3
        // responds with 4xx
        //
        // In addition, we only set the range if the download was *NOT*
        // determined to be parallelizable above. One of the conditions for
        // parallel downloads is that getRange() returns null so preserve that.
        if (totalBytesToDownload > 0 && !isDownloadParallel) {
            getObjectRequest.withRange(startingByte, lastByte);
        }

        long fileLength = -1;

        if (resumeExistingDownload) {
            if (isS3ObjectModifiedSincePause(lastModifiedTime, lastModifiedTimeRecordedDuringPause)) {
                throw new AmazonClientException("The requested object in bucket " + getObjectRequest.getBucketName()
                        + " with key " + getObjectRequest.getKey() + " is modified on Amazon S3 since the last pause.");
            }

            if (etag != null && lastEtagDuringPause != null) {
                //this check is done for backward compatible were etag is not stubbed in getObjectMetaData response
                // lastEtagDuringPause can be null if downloads were paused when Etag was not persisted as a part of pause.
                if (isS3ObjectEtagModifiedSincePause(etag, lastEtagDuringPause)) {
                    throw new AmazonClientException(
                            String.format("The requested object in bucket '%s' with key '%s' has been modified " +
                                            "on Amazon S3 since the last pause. Expected ETag: %s, Found: %s",
                                    getObjectRequest.getBucketName(), getObjectRequest.getKey(),
                                    lastEtagDuringPause, etag)
                    );

                }
            }
            // There's still a chance the object is modified while the request
            // is in flight. Set this header so S3 fails the request if this happens.
            getObjectRequest.setUnmodifiedSinceConstraint(new Date(lastModifiedTime));

            if (!isDownloadParallel) {
                if (!FileLocks.lock(file)) {
                    throw new FileLockException("Fail to lock " + file + " for resume download");
                }
                try {
                    if (file.exists()) {
                        fileLength = file.length();
                        startingByte = startingByte + fileLength;
                        getObjectRequest.setRange(startingByte, lastByte);
                        transferProgress.updateProgress(Math.min(fileLength, totalBytesToDownload));
                        totalBytesToDownload = lastByte - startingByte + 1;
                        if (log.isDebugEnabled()) {
                            log.debug("Resume download: totalBytesToDownload=" + totalBytesToDownload
                                    + ", origStartingByte=" + origStartingByte + ", startingByte=" + startingByte
                                    + ", lastByte=" + lastByte + ", numberOfBytesRead=" + fileLength + ", file: "
                                    + file);
                        }
                    }
                } finally {
                    FileLocks.unlock(file);
                }
            }
        }

        if (totalBytesToDownload < 0) {
            throw new IllegalArgumentException(
                    "Unable to determine the range for download operation.");
        }

        final CountDownLatch latch = new CountDownLatch(1);

        DownloadCallable downloadCallable = new DownloadCallable(s3, latch,
                getObjectRequest, resumeExistingDownload,
                download, file, origStartingByte, fileLength, timeoutMillis, timedThreadPool,
                executorService, lastFullyDownloadedPart, isDownloadParallel, resumeOnRetry)
                .withLastFullyMergedPartPosition(lastFullyDownloadedPartPosition);

        return new PreparedDownloadContext(download, downloadCallable, latch);
    }
Same as public interface, but adds a state listener so that callers can
be notified of state changes to the download.

@see TransferManager#download(GetObjectRequest, File)
aws-java-sdk-s3/src/main/java/com/amazonaws/services/s3/transfer/TransferManager.java