fr.bmartel.speedtest.SpeedTestTask.java Source code

Java tutorial

Introduction

Here is the source code for fr.bmartel.speedtest.SpeedTestTask.java

Source

/*
 * The MIT License (MIT)
 * <p/>
 * Copyright (c) 2016 Bertrand Martel
 * <p/>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * <p/>
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * <p/>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package fr.bmartel.speedtest;

import fr.bmartel.protocol.http.HttpFrame;
import fr.bmartel.protocol.http.states.HttpStates;
import fr.bmartel.speedtest.inter.ISpeedTestListener;
import fr.bmartel.speedtest.inter.ISpeedTestSocket;
import fr.bmartel.speedtest.model.SpeedTestError;
import fr.bmartel.speedtest.model.SpeedTestMode;
import fr.bmartel.speedtest.model.UploadStorageType;
import fr.bmartel.speedtest.utils.RandomGen;
import fr.bmartel.speedtest.utils.SpeedTestUtils;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.List;
import java.util.concurrent.*;

/**
 * This class manage all download/upload operations.
 *
 * @author Bertrand Martel
 */
public class SpeedTestTask {

    /**
     * socket server hostname.
     */
    private String mHostname = "";

    /**
     * socket server port.
     */
    private int mPort;

    /**
     * socket object.
     */
    private Socket mSocket;

    /**
     * start time triggered in millis.
     */
    private long mTimeStart;

    /**
     * end time triggered in millis.
     */
    private long mTimeEnd;

    /**
     * this is the number of bit uploaded at this time.
     */
    private int mUploadTempFileSize;

    /**
     * this is the number of packet downloaded at this time.
     */
    private int mDownloadTemporaryPacketSize;

    /**
     * this is the number of packet to download.
     */
    private BigDecimal mDownloadPckSize = BigDecimal.ZERO;

    /**
     * FTP inputstream.
     */
    private InputStream mFtpInputstream;

    /**
     * FTP outputstream.
     */
    private OutputStream mFtpOutputstream;

    /**
     * define if an error has been dispatched already or not. This is reset to false on start download/ upload + in
     * reading thread
     */
    private boolean mErrorDispatched;

    /**
     * define if mSocket close error is to be expected.
     */
    private boolean mForceCloseSocket;

    /**
     * size of file to upload.
     */
    private BigDecimal mUploadFileSize = BigDecimal.ZERO;

    /**
     * SpeedTestSocket interface.
     */
    private final ISpeedTestSocket mSocketInterface;

    /**
     * Speed test repeat wrapper.
     */
    private final RepeatWrapper mRepeatWrapper;

    /**
     * Listener list.
     */
    private final List<ISpeedTestListener> mListenerList;

    /**
     * define if report interval is set.
     */
    private boolean mReportInterval;

    /**
     * executor service for reading operation.
     */
    private ExecutorService mReadExecutorService;

    /**
     * executor service for writing operation.
     */
    private ExecutorService mWriteExecutorService;

    /**
     * executor service used for reporting.
     */
    private ScheduledExecutorService mReportExecutorService;

    /**
     * current speed test mode.
     */
    private SpeedTestMode mSpeedTestMode = SpeedTestMode.NONE;

    /**
     * milliseconds divider for calculating the threshold before updating the calculation of the upload speed
     */
    private static final long MILLISECONDS_DIVIDER = 1000;

    /**
     * Build socket.
     *
     * @param socketInterface interface shared between repeat wrapper and speed test socket
     */
    public SpeedTestTask(final ISpeedTestSocket socketInterface, final List<ISpeedTestListener> listenerList) {
        mSocketInterface = socketInterface;
        mRepeatWrapper = mSocketInterface.getRepeatWrapper();
        mListenerList = listenerList;
        initThreadPool();
    }

    /**
     * initialize thread pool.
     */
    private void initThreadPool() {
        mReadExecutorService = Executors.newSingleThreadExecutor();
        mReportExecutorService = Executors.newScheduledThreadPool(SpeedTestConst.THREAD_POOL_REPORT_SIZE);
        mWriteExecutorService = Executors.newSingleThreadExecutor();
    }

    /**
     * Set report interval state.
     *
     * @param state define if a report interval is set
     */
    public void setReportInterval(final boolean state) {
        mReportInterval = state;
    }

    /**
     * start download task.
     *
     * @param hostname server mHostname
     * @param port     server mPort
     * @param uri      uri to fetch to download file
     */
    public void startDownloadRequest(final String hostname, final int port, final String uri) {

        mSpeedTestMode = SpeedTestMode.DOWNLOAD;

        mForceCloseSocket = false;
        mErrorDispatched = false;
        this.mHostname = hostname;
        this.mPort = port;
        final String downloadRequest = "GET " + uri + " HTTP/1.1\r\n" + "Host: " + hostname + "\r\n\r\n";

        writeDownload(downloadRequest.getBytes());
    }

    /**
     * shutdown executors to release threads.
     */
    private void closeExecutors() {
        mReadExecutorService.shutdownNow();
        mReportExecutorService.shutdownNow();
        mWriteExecutorService.shutdownNow();
    }

    /**
     * Write upload POST request with file generated randomly.
     */
    public void writeUpload(final String hostname, final int port, final String uri, final int fileSizeOctet) {

        mSpeedTestMode = SpeedTestMode.UPLOAD;

        this.mHostname = hostname;
        this.mPort = port;
        mUploadFileSize = new BigDecimal(fileSizeOctet);
        mForceCloseSocket = false;
        mErrorDispatched = false;
        mUploadTempFileSize = 0;
        mTimeStart = System.currentTimeMillis();

        connectAndExecuteTask(new Runnable() {
            @Override
            public void run() {
                if (mSocket != null && !mSocket.isClosed()) {

                    RandomAccessFile uploadFile = null;
                    final RandomGen randomGen = new RandomGen();

                    try {

                        byte[] body = new byte[] {};

                        if (mSocketInterface.getUploadStorageType() == UploadStorageType.RAM_STORAGE) {
                            /* generate a file with size of fileSizeOctet octet */
                            body = randomGen.generateRandomArray(fileSizeOctet);
                        } else {
                            uploadFile = randomGen.generateRandomFile(fileSizeOctet);
                            uploadFile.seek(0);
                        }

                        final String head = "POST " + uri + " HTTP/1.1\r\n" + "Host: " + hostname + "\r\nAccept: "
                                + "*/*\r\nContent-Length: " + fileSizeOctet + "\r\n\r\n";

                        mUploadTempFileSize = 0;

                        final int uploadChunkSize = mSocketInterface.getUploadChunkSize();

                        final int step = fileSizeOctet / uploadChunkSize;
                        final int remain = fileSizeOctet % uploadChunkSize;

                        if (mSocket.getOutputStream() != null) {

                            if (writeFlushSocket(head.getBytes()) != 0) {
                                throw new SocketTimeoutException();
                            }

                            mTimeStart = System.currentTimeMillis();
                            mTimeEnd = 0;

                            if (mRepeatWrapper.isFirstUpload()) {
                                mRepeatWrapper.setFirstUploadRepeat(false);
                                mRepeatWrapper.setStartDate(mTimeStart);
                            }

                            if (mRepeatWrapper.isRepeatUpload()) {
                                mRepeatWrapper.updatePacketSize(mUploadFileSize);
                            }

                            for (int i = 0; i < step; i++) {

                                final byte[] chunk = SpeedTestUtils.readUploadData(
                                        mSocketInterface.getUploadStorageType(), body, uploadFile,
                                        mUploadTempFileSize, uploadChunkSize);

                                if (writeFlushSocket(chunk) != 0) {
                                    throw new SocketTimeoutException();
                                }

                                mUploadTempFileSize += uploadChunkSize;

                                if (mRepeatWrapper.isRepeatUpload()) {
                                    mRepeatWrapper.updateTempPacketSize(uploadChunkSize);
                                }

                                if (!mReportInterval) {
                                    final SpeedTestReport report = mSocketInterface.getLiveUploadReport();

                                    for (int j = 0; j < mListenerList.size(); j++) {
                                        mListenerList.get(j).onUploadProgress(report.getProgressPercent(), report);
                                    }
                                }
                            }

                            final byte[] chunk = SpeedTestUtils.readUploadData(
                                    mSocketInterface.getUploadStorageType(), body, uploadFile, mUploadTempFileSize,
                                    remain);

                            if (remain != 0 && writeFlushSocket(chunk) != 0) {
                                throw new SocketTimeoutException();
                            } else {
                                mUploadTempFileSize += remain;

                                if (mRepeatWrapper.isRepeatUpload()) {
                                    mRepeatWrapper.updateTempPacketSize(remain);
                                }
                            }

                            if (!mReportInterval) {
                                final SpeedTestReport report = mSocketInterface.getLiveUploadReport();

                                for (int j = 0; j < mListenerList.size(); j++) {
                                    mListenerList.get(j).onUploadProgress(SpeedTestConst.PERCENT_MAX.floatValue(),
                                            report);

                                }
                            }
                        }
                    } catch (SocketTimeoutException e) {
                        mReportInterval = false;
                        mErrorDispatched = true;
                        closeSocket();
                        closeExecutors();
                        if (!mForceCloseSocket) {
                            SpeedTestUtils.dispatchSocketTimeout(mForceCloseSocket, mListenerList, false,
                                    SpeedTestConst.SOCKET_WRITE_ERROR);
                        } else {
                            SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, false, e.getMessage());
                        }
                    } catch (IOException e) {
                        mReportInterval = false;
                        mErrorDispatched = true;
                        closeExecutors();
                        SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, false, e.getMessage());
                    } finally {
                        if (uploadFile != null) {
                            try {
                                uploadFile.close();
                                randomGen.deleteFile();
                            } catch (IOException e) {
                                //e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }, false);
    }

    /**
     * Create and connect mSocket.
     *
     * @param task     task to be executed when connected to mSocket
     * @param download define if it is a download or upload test
     */
    private void connectAndExecuteTask(final Runnable task, final boolean download) {

        // close mSocket before recreating it
        if (mSocket != null) {
            closeSocket();
        }
        try {
            /* create a basic mSocket connection */
            mSocket = new Socket();

            if (mSocketInterface.getSocketTimeout() != 0 && download) {
                mSocket.setSoTimeout(mSocketInterface.getSocketTimeout());
            }

            /* establish mSocket parameters */
            mSocket.setReuseAddress(true);

            mSocket.setKeepAlive(true);

            mSocket.connect(new InetSocketAddress(mHostname, mPort));

            if (mReadExecutorService == null || mReadExecutorService.isShutdown()) {
                mReadExecutorService = Executors.newSingleThreadExecutor();
            }

            mReadExecutorService.execute(new Runnable() {

                @Override
                public void run() {

                    if (download) {
                        startSocketDownloadTask();
                    } else {
                        startSocketUploadTask();
                    }
                    mSpeedTestMode = SpeedTestMode.NONE;
                }
            });

            if (mWriteExecutorService == null || mWriteExecutorService.isShutdown()) {
                mWriteExecutorService = Executors.newSingleThreadExecutor();
            }

            mWriteExecutorService.execute(new Runnable() {
                @Override
                public void run() {
                    if (task != null) {
                        task.run();
                    }
                }
            });

        } catch (IOException e) {
            if (!mErrorDispatched) {
                SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, download, e.getMessage());
            }
        }
    }

    /**
     * start download reading task.
     */
    private void startSocketDownloadTask() {

        mDownloadTemporaryPacketSize = 0;

        try {
            final HttpFrame httpFrame = new HttpFrame();

            mTimeStart = System.currentTimeMillis();
            mTimeEnd = 0;

            if (mRepeatWrapper.isFirstDownload()) {
                mRepeatWrapper.setFirstDownloadRepeat(false);
                mRepeatWrapper.setStartDate(mTimeStart);
            }

            final HttpStates httFrameState = httpFrame.decodeFrame(mSocket.getInputStream());

            SpeedTestUtils.checkHttpFrameError(mForceCloseSocket, mListenerList, httFrameState);

            final HttpStates httpHeaderState = httpFrame.parseHeader(mSocket.getInputStream());
            SpeedTestUtils.checkHttpHeaderError(mForceCloseSocket, mListenerList, httpHeaderState);

            SpeedTestUtils.checkHttpContentLengthError(mForceCloseSocket, mListenerList, httpFrame);

            if (httpFrame.getStatusCode() == SpeedTestConst.HTTP_OK
                    && httpFrame.getReasonPhrase().equalsIgnoreCase("ok")) {

                mDownloadPckSize = new BigDecimal(httpFrame.getContentLength());

                if (mRepeatWrapper.isRepeatDownload()) {
                    mRepeatWrapper.updatePacketSize(mDownloadPckSize);
                }

                downloadReadingLoop();
                mTimeEnd = System.currentTimeMillis();

                closeSocket();

                mReportInterval = false;

                if (!mRepeatWrapper.isRepeatDownload()) {
                    closeExecutors();
                }

                final SpeedTestReport report = mSocketInterface.getLiveDownloadReport();

                for (int i = 0; i < mListenerList.size(); i++) {
                    mListenerList.get(i).onDownloadFinished(report);
                }

            } else {

                mReportInterval = false;

                for (int i = 0; i < mListenerList.size(); i++) {
                    mListenerList.get(i).onDownloadError(SpeedTestError.INVALID_HTTP_RESPONSE,
                            "Error status code " + httpFrame.getStatusCode());
                }

                closeSocket();

                if (!mRepeatWrapper.isRepeatDownload()) {
                    closeExecutors();
                }
            }

        } catch (SocketTimeoutException e) {
            mReportInterval = false;
            SpeedTestUtils.dispatchSocketTimeout(mForceCloseSocket, mListenerList, true, e.getMessage());
            mTimeEnd = System.currentTimeMillis();
            closeSocket();
            closeExecutors();
        } catch (IOException | InterruptedException e) {
            mReportInterval = false;
            catchError(true, e.getMessage());
        }
        mErrorDispatched = false;
    }

    /**
     * start download reading loop + monitor progress.
     *
     * @throws IOException mSocket io exception
     */
    private void downloadReadingLoop() throws IOException {

        final byte[] buffer = new byte[SpeedTestConst.READ_BUFFER_SIZE];
        int read;

        while ((read = mSocket.getInputStream().read(buffer)) != -1) {

            mDownloadTemporaryPacketSize += read;

            if (mRepeatWrapper.isRepeatDownload()) {
                mRepeatWrapper.updateTempPacketSize(read);
            }

            if (!mReportInterval) {
                final SpeedTestReport report = mSocketInterface.getLiveDownloadReport();
                for (int i = 0; i < mListenerList.size(); i++) {
                    mListenerList.get(i).onDownloadProgress(report.getProgressPercent(), report);
                }
            }

            if (mDownloadTemporaryPacketSize == mDownloadPckSize.longValueExact()) {
                break;
            }
        }
    }

    /**
     * start upload writing task.
     */
    private void startSocketUploadTask() {

        try {
            final HttpFrame frame = new HttpFrame();

            final HttpStates httpStates = frame.parseHttp(mSocket.getInputStream());

            if (httpStates == HttpStates.HTTP_FRAME_OK) {

                if (frame.getStatusCode() == SpeedTestConst.HTTP_OK
                        && frame.getReasonPhrase().equalsIgnoreCase("ok")) {

                    mTimeEnd = System.currentTimeMillis();

                    closeSocket();
                    mReportInterval = false;

                    if (!mRepeatWrapper.isRepeatUpload()) {
                        closeExecutors();
                    }

                    final SpeedTestReport report = mSocketInterface.getLiveUploadReport();

                    for (int i = 0; i < mListenerList.size(); i++) {
                        mListenerList.get(i).onUploadFinished(report);
                    }

                } else {
                    mReportInterval = false;

                    for (int i = 0; i < mListenerList.size(); i++) {
                        mListenerList.get(i).onUploadError(SpeedTestError.INVALID_HTTP_RESPONSE,
                                "Error status code" + " " + frame.getStatusCode());
                    }

                    closeSocket();

                    if (!mRepeatWrapper.isRepeatUpload()) {
                        closeExecutors();
                    }
                }
                return;
            }
            closeSocket();
            if (!mErrorDispatched && !mForceCloseSocket) {
                for (int i = 0; i < mListenerList.size(); i++) {
                    mListenerList.get(i).onUploadError(SpeedTestError.SOCKET_ERROR, "mSocket error");
                }
            }
            closeExecutors();
        } catch (IOException | InterruptedException e) {
            mReportInterval = false;
            if (!mErrorDispatched) {
                catchError(false, e.getMessage());
            }
        }
        mErrorDispatched = false;
    }

    /**
     * Write download request to server host.
     *
     * @param data HTTP request to send to initiate download process
     */
    private void writeDownload(final byte[] data) {

        connectAndExecuteTask(new Runnable() {
            @Override
            public void run() {

                if (mSocket != null && !mSocket.isClosed()) {

                    try {
                        if ((mSocket.getOutputStream() != null) && (writeFlushSocket(data) != 0)) {
                            throw new SocketTimeoutException();
                        }
                    } catch (SocketTimeoutException e) {
                        SpeedTestUtils.dispatchSocketTimeout(mForceCloseSocket, mListenerList, true,
                                SpeedTestConst.SOCKET_WRITE_ERROR);
                        closeSocket();
                        closeExecutors();
                    } catch (IOException e) {
                        SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, true, e.getMessage());
                        closeExecutors();
                    }
                }
            }
        }, true);
    }

    /**
     * logout & disconnect FTP client.
     *
     * @param ftpclient ftp client
     */
    private void disconnectFtp(final FTPClient ftpclient) {

        try {
            if (ftpclient.isConnected()) {
                ftpclient.logout();
                ftpclient.disconnect();
            }
        } catch (IOException ex) {
            //ex.printStackTrace();
        }
    }

    /**
     * write and flush mSocket.
     *
     * @param data payload to write
     * @return error status (-1 for error)
     * @throws IOException mSocket io exception
     */
    private int writeFlushSocket(final byte[] data) throws IOException {

        final ExecutorService executor = Executors.newSingleThreadExecutor();

        @SuppressWarnings("unchecked")
        final Future<Integer> future = executor.submit(new Callable() {

            /**
             * execute sequential write/flush task.
             *
             * @return status
             */
            public Integer call() {
                try {
                    mSocket.getOutputStream().write(data);
                    mSocket.getOutputStream().flush();
                } catch (IOException e) {
                    return -1;
                }
                return 0;
            }
        });
        int status;
        try {
            status = future.get(mSocketInterface.getSocketTimeout(), TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            future.cancel(true);
            status = -1;
        } catch (InterruptedException | ExecutionException e) {
            status = -1;
        }
        executor.shutdownNow();
        return status;
    }

    /**
     * catch an error.
     *
     * @param isDownload   downloading task or uploading task
     * @param errorMessage error message from Exception
     */
    private void catchError(final boolean isDownload, final String errorMessage) {
        SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, isDownload, errorMessage);
        mTimeEnd = System.currentTimeMillis();
        closeSocket();
        closeExecutors();
    }

    /**
     * get a download/upload report.
     *
     * @param mode speed test mode requested
     * @return speed test report
     */
    public SpeedTestReport getReport(final SpeedTestMode mode) {

        BigDecimal temporaryPacketSize = BigDecimal.ZERO;
        BigDecimal totalPacketSize = BigDecimal.ZERO;

        switch (mode) {
        case DOWNLOAD:
            temporaryPacketSize = new BigDecimal(mDownloadTemporaryPacketSize);
            totalPacketSize = mDownloadPckSize;
            break;
        case UPLOAD:
            temporaryPacketSize = new BigDecimal(mUploadTempFileSize);
            totalPacketSize = mUploadFileSize;
            break;
        default:
            break;
        }

        long currentTime;
        if (mTimeEnd == 0) {
            currentTime = System.currentTimeMillis();
        } else {
            currentTime = mTimeEnd;
        }

        BigDecimal transferRateOps = BigDecimal.ZERO;

        final int scale = mSocketInterface.getDefaultScale();
        final RoundingMode roundingMode = mSocketInterface.getDefaultRoundingMode();

        if (shallCalculateTransferRate(currentTime, mode)) {
            transferRateOps = temporaryPacketSize.divide(new BigDecimal(currentTime - mTimeStart)
                    .divide(SpeedTestConst.MILLIS_DIVIDER, scale, roundingMode), scale, roundingMode);
        }

        final BigDecimal transferRateBitps = transferRateOps.multiply(SpeedTestConst.BIT_MULTIPLIER);

        BigDecimal percent = BigDecimal.ZERO;

        SpeedTestReport report;

        if (mRepeatWrapper.isRepeat()) {

            report = mRepeatWrapper.getRepeatReport(scale, roundingMode, mode, currentTime, transferRateOps);

        } else {

            if (totalPacketSize != BigDecimal.ZERO) {

                percent = temporaryPacketSize.multiply(SpeedTestConst.PERCENT_MAX).divide(totalPacketSize, scale,
                        roundingMode);
            }

            report = new SpeedTestReport(mode, percent.floatValue(), mTimeStart, currentTime,
                    temporaryPacketSize.longValueExact(), totalPacketSize.longValueExact(), transferRateOps,
                    transferRateBitps, 1);
        }
        return report;
    }

    private boolean shallCalculateTransferRate(final long currentTime, final SpeedTestMode mode) {
        final long elapsedTime = currentTime - mTimeStart;
        boolean response;

        switch (mode) {
        case DOWNLOAD:
            response = elapsedTime != 0;
            break;
        case UPLOAD:
        default:
            response = elapsedTime > MILLISECONDS_DIVIDER;
            break;
        }

        return response;
    }

    /**
     * Get FTP file size.
     *
     * @param ftpClient ftp client
     * @param filePath  remote file path
     * @return file size
     * @throws Exception file read/write IOException
     */
    private long getFileSize(final FTPClient ftpClient, final String filePath) throws IOException {

        long fileSize = 0;
        final FTPFile[] files = ftpClient.listFiles(filePath);
        if (files.length == 1 && files[0].isFile()) {
            fileSize = files[0].getSize();
        }
        return fileSize;
    }

    /**
     * start FTP download with specific port, user, password.
     *
     * @param hostname ftp host
     * @param uri      ftp uri
     * @param user     ftp username
     * @param password ftp password
     */
    public void startFtpDownload(final String hostname, final int port, final String uri, final String user,
            final String password) {

        mSpeedTestMode = SpeedTestMode.DOWNLOAD;

        mErrorDispatched = false;
        mForceCloseSocket = false;

        if (mReadExecutorService == null || mReadExecutorService.isShutdown()) {
            mReadExecutorService = Executors.newSingleThreadExecutor();
        }

        mReadExecutorService.execute(new Runnable() {

            @Override
            public void run() {

                final FTPClient ftpclient = new FTPClient();

                try {
                    ftpclient.connect(hostname, port);
                    ftpclient.login(user, password);
                    ftpclient.enterLocalPassiveMode();
                    ftpclient.setFileType(FTP.BINARY_FILE_TYPE);

                    mDownloadTemporaryPacketSize = 0;

                    mTimeStart = System.currentTimeMillis();
                    mTimeEnd = 0;

                    if (mRepeatWrapper.isFirstDownload()) {
                        mRepeatWrapper.setFirstDownloadRepeat(false);
                        mRepeatWrapper.setStartDate(mTimeStart);
                    }

                    mDownloadPckSize = new BigDecimal(getFileSize(ftpclient, uri));

                    if (mRepeatWrapper.isRepeatDownload()) {
                        mRepeatWrapper.updatePacketSize(mDownloadPckSize);
                    }

                    mFtpInputstream = ftpclient.retrieveFileStream(uri);

                    if (mFtpInputstream != null) {

                        final byte[] bytesArray = new byte[SpeedTestConst.READ_BUFFER_SIZE];

                        int read;
                        while ((read = mFtpInputstream.read(bytesArray)) != -1) {
                            mDownloadTemporaryPacketSize += read;

                            if (mRepeatWrapper.isRepeatDownload()) {
                                mRepeatWrapper.updateTempPacketSize(read);
                            }

                            if (!mReportInterval) {
                                final SpeedTestReport report = mSocketInterface.getLiveDownloadReport();

                                for (int i = 0; i < mListenerList.size(); i++) {
                                    mListenerList.get(i).onDownloadProgress(report.getProgressPercent(), report);
                                }
                            }

                            if (mDownloadTemporaryPacketSize == mDownloadPckSize.longValueExact()) {
                                break;
                            }
                        }

                        mFtpInputstream.close();

                        mTimeEnd = System.currentTimeMillis();

                        mReportInterval = false;
                        final SpeedTestReport report = mSocketInterface.getLiveDownloadReport();

                        for (int i = 0; i < mListenerList.size(); i++) {
                            mListenerList.get(i).onDownloadFinished(report);
                        }

                    } else {

                        mReportInterval = false;
                        SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, true, "cant create stream "
                                + "from uri " + uri + " with reply code : " + ftpclient.getReplyCode());
                    }

                    if (!mRepeatWrapper.isRepeatDownload()) {
                        closeExecutors();
                    }

                } catch (IOException e) {
                    //e.printStackTrace();
                    mReportInterval = false;
                    catchError(true, e.getMessage());
                } finally {
                    mErrorDispatched = false;
                    mSpeedTestMode = SpeedTestMode.NONE;
                    disconnectFtp(ftpclient);
                }
            }
        });
    }

    /**
     * Start FTP upload.
     *
     * @param hostname      ftp host
     * @param port          ftp port
     * @param uri           upload uri
     * @param fileSizeOctet file size in octet
     * @param user          username
     * @param password      password
     */
    public void startFtpUpload(final String hostname, final int port, final String uri, final int fileSizeOctet,
            final String user, final String password) {

        mSpeedTestMode = SpeedTestMode.UPLOAD;

        mUploadFileSize = new BigDecimal(fileSizeOctet);
        mForceCloseSocket = false;
        mErrorDispatched = false;

        if (mWriteExecutorService == null || mWriteExecutorService.isShutdown()) {
            mWriteExecutorService = Executors.newSingleThreadExecutor();
        }

        mWriteExecutorService.execute(new Runnable() {
            @Override
            public void run() {

                final FTPClient ftpClient = new FTPClient();
                final RandomGen randomGen = new RandomGen();

                RandomAccessFile uploadFile = null;

                try {
                    ftpClient.connect(hostname, port);
                    ftpClient.login(user, password);
                    ftpClient.enterLocalPassiveMode();
                    ftpClient.setFileType(FTP.BINARY_FILE_TYPE);

                    byte[] fileContent = new byte[] {};

                    if (mSocketInterface.getUploadStorageType() == UploadStorageType.RAM_STORAGE) {
                        /* generate a file with size of fileSizeOctet octet */
                        fileContent = randomGen.generateRandomArray(fileSizeOctet);
                    } else {
                        uploadFile = randomGen.generateRandomFile(fileSizeOctet);
                        uploadFile.seek(0);
                    }

                    mFtpOutputstream = ftpClient.storeFileStream(uri);

                    if (mFtpOutputstream != null) {

                        mUploadTempFileSize = 0;

                        final int uploadChunkSize = mSocketInterface.getUploadChunkSize();

                        final int step = fileSizeOctet / uploadChunkSize;
                        final int remain = fileSizeOctet % uploadChunkSize;

                        mTimeStart = System.currentTimeMillis();
                        mTimeEnd = 0;

                        if (mRepeatWrapper.isFirstUpload()) {
                            mRepeatWrapper.setFirstUploadRepeat(false);
                            mRepeatWrapper.setStartDate(mTimeStart);
                        }

                        if (mRepeatWrapper.isRepeatUpload()) {
                            mRepeatWrapper.updatePacketSize(mUploadFileSize);
                        }

                        if (mForceCloseSocket) {
                            SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, false, "");
                        } else {
                            for (int i = 0; i < step; i++) {

                                final byte[] chunk = SpeedTestUtils.readUploadData(
                                        mSocketInterface.getUploadStorageType(), fileContent, uploadFile,
                                        mUploadTempFileSize, uploadChunkSize);

                                mFtpOutputstream.write(chunk, 0, uploadChunkSize);

                                mUploadTempFileSize += uploadChunkSize;

                                if (mRepeatWrapper.isRepeatUpload()) {
                                    mRepeatWrapper.updateTempPacketSize(uploadChunkSize);
                                }

                                if (!mReportInterval) {

                                    final SpeedTestReport report = mSocketInterface.getLiveUploadReport();

                                    for (int j = 0; j < mListenerList.size(); j++) {
                                        mListenerList.get(j).onUploadProgress(report.getProgressPercent(), report);
                                    }
                                }
                            }

                            if (remain != 0) {

                                final byte[] chunk = SpeedTestUtils.readUploadData(
                                        mSocketInterface.getUploadStorageType(), fileContent, uploadFile,
                                        mUploadTempFileSize, remain);

                                mFtpOutputstream.write(chunk, 0, remain);

                                mUploadTempFileSize += remain;

                                if (mRepeatWrapper.isRepeatUpload()) {
                                    mRepeatWrapper.updateTempPacketSize(remain);
                                }
                            }
                            if (!mReportInterval) {
                                final SpeedTestReport report = mSocketInterface.getLiveUploadReport();

                                for (int j = 0; j < mListenerList.size(); j++) {
                                    mListenerList.get(j).onUploadProgress(SpeedTestConst.PERCENT_MAX.floatValue(),
                                            report);

                                }
                            }
                            mTimeEnd = System.currentTimeMillis();
                        }
                        mFtpOutputstream.close();

                        mReportInterval = false;
                        final SpeedTestReport report = mSocketInterface.getLiveUploadReport();

                        for (int i = 0; i < mListenerList.size(); i++) {
                            mListenerList.get(i).onUploadFinished(report);
                        }

                        if (!mRepeatWrapper.isRepeatUpload()) {
                            closeExecutors();
                        }

                    } else {
                        mReportInterval = false;
                        SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, false, "cant create stream "
                                + "from uri " + uri + " with reply code : " + ftpClient.getReplyCode());
                    }
                } catch (SocketTimeoutException e) {
                    //e.printStackTrace();
                    mReportInterval = false;
                    mErrorDispatched = true;
                    if (!mForceCloseSocket) {
                        SpeedTestUtils.dispatchSocketTimeout(mForceCloseSocket, mListenerList, false,
                                SpeedTestConst.SOCKET_WRITE_ERROR);
                    } else {
                        SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, false, e.getMessage());
                    }
                    closeSocket();
                    closeExecutors();
                } catch (IOException e) {
                    //e.printStackTrace();
                    mReportInterval = false;
                    mErrorDispatched = true;
                    SpeedTestUtils.dispatchError(mForceCloseSocket, mListenerList, false, e.getMessage());
                    closeExecutors();
                } finally {
                    mErrorDispatched = false;
                    mSpeedTestMode = SpeedTestMode.NONE;
                    disconnectFtp(ftpClient);
                    if (uploadFile != null) {
                        try {
                            uploadFile.close();
                            randomGen.deleteFile();
                        } catch (IOException e) {
                            //e.printStackTrace();
                        }
                    }
                }
            }
        });
    }

    /**
     * Close socket streams and mSocket object.
     */
    public void closeSocket() {

        if (mSocket != null) {
            try {
                mSocket.close();
            } catch (IOException e) {
            }
        }
    }

    /**
     * close socket / stop download/upload operations.
     */
    public void forceStopTask() {
        mSpeedTestMode = SpeedTestMode.NONE;
        mForceCloseSocket = true;
        if (mFtpInputstream != null) {
            try {
                mFtpInputstream.close();
            } catch (IOException e) {
                //e.printStackTrace();
            }
        }
        if (mFtpOutputstream != null) {
            try {
                mFtpOutputstream.close();
            } catch (IOException e) {
                //e.printStackTrace();
            }
        }
    }

    /**
     * Shutdown threadpool and wait for task completion.
     */
    public void shutdownAndWait() {
        closeExecutors();
        try {
            mReadExecutorService.awaitTermination(SpeedTestConst.THREADPOOL_WAIT_COMPLETION_MS,
                    TimeUnit.MILLISECONDS);
            mWriteExecutorService.awaitTermination(SpeedTestConst.THREADPOOL_WAIT_COMPLETION_MS,
                    TimeUnit.MILLISECONDS);
            mReportExecutorService.awaitTermination(SpeedTestConst.THREADPOOL_WAIT_COMPLETION_MS,
                    TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            //e.printStackTrace();
        }
    }

    /**
     * reset report threadpool if necessary.
     */
    public void renewReportThreadPool() {
        if (mReportExecutorService == null || mReportExecutorService.isShutdown()) {
            mReportExecutorService = Executors.newScheduledThreadPool(SpeedTestConst.THREAD_POOL_REPORT_SIZE);
        }
    }

    /**
     * retrieve threadpool used to publish reports.
     *
     * @return report threadpool
     */
    public ScheduledExecutorService getReportThreadPool() {
        return mReportExecutorService;
    }

    /**
     * retrieve current speed test mode.
     *
     * @return speed test mode (UPLOAD/DOWNLOAD/NONE)
     */
    public SpeedTestMode getSpeedTestMode() {
        return mSpeedTestMode;
    }
}