/*
 * Copyright (c) 2015 LingoChamp Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.liulishuo.filedownloader.download;

import android.Manifest;
import android.os.Process;

import com.liulishuo.filedownloader.DownloadTask;
import com.liulishuo.filedownloader.IThreadPoolMonitor;
import com.liulishuo.filedownloader.connection.FileDownloadConnection;
import com.liulishuo.filedownloader.database.FileDownloadDatabase;
import com.liulishuo.filedownloader.exception.FileDownloadGiveUpRetryException;
import com.liulishuo.filedownloader.exception.FileDownloadHttpException;
import com.liulishuo.filedownloader.exception.FileDownloadNetworkPolicyException;
import com.liulishuo.filedownloader.exception.FileDownloadOutOfSpaceException;
import com.liulishuo.filedownloader.exception.FileDownloadSecurityException;
import com.liulishuo.filedownloader.model.ConnectionModel;
import com.liulishuo.filedownloader.model.FileDownloadHeader;
import com.liulishuo.filedownloader.model.FileDownloadModel;
import com.liulishuo.filedownloader.model.FileDownloadStatus;
import com.liulishuo.filedownloader.stream.FileDownloadOutputStream;
import com.liulishuo.filedownloader.util.FileDownloadExecutors;
import com.liulishuo.filedownloader.util.FileDownloadHelper;
import com.liulishuo.filedownloader.util.FileDownloadLog;
import com.liulishuo.filedownloader.util.FileDownloadProperties;
import com.liulishuo.filedownloader.util.FileDownloadUtils;

import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * You can use this to launch downloading, on here the download will be launched separate following
 * steps:
 *

 * step 1. create the trial connection
 *          ( this trial connection is used for:
 *                  1. checkup the saved etag is overdue
 *                  2. checkup whether the partial-accept is supported
 *                  3. checkup whether the current connection is chunked. )
 *
 * step 2. if the saved etag is overdue -> jump to step 1 to checkup whether the partial-accept is
 * supported.
 * step 3. if (NOT chunked) & partial-accept & output stream support-seek:
 *              create multiple {@link DownloadTask} to download.
 *         else:
 *              create single first connection and use {@link FetchDataTask} to fetch data from the
 *              connection.
 *

 * We use {@link DownloadStatusCallback} to handle all events sync to DB/filesystem and callback to
 * user.
 */
public class DownloadLaunchRunnable implements Runnable, ProcessCallback {

    private final DownloadStatusCallback statusCallback;
    private final int defaultConnectionCount = 5;
    private final FileDownloadModel model;
    private final FileDownloadHeader userRequestHeader;
    private final boolean isForceReDownload;
    private final boolean isWifiRequired;

    private final FileDownloadDatabase database;
    private final IThreadPoolMonitor threadPoolMonitor;

    private boolean isTriedFixRangeNotSatisfiable;

    int validRetryTimes;

    /**
     * None of the ranges in the request's Range header field overlap the current extent of the
     * selected resource or that the set of ranges requested has been rejected due to invalid
     * ranges or an excessive request of small or overlapping ranges.
     */
    private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
    private static final int TOTAL_VALUE_IN_CHUNKED_RESOURCE = -1;

    private boolean isNeedForceDiscardRange = false;

    private final boolean supportSeek;

    private final ArrayList<DownloadRunnable> downloadRunnableList = new ArrayList<>(
            defaultConnectionCount);
    private DownloadRunnable singleDownloadRunnable;
    private boolean isSingleConnection;

    private static final ThreadPoolExecutor DOWNLOAD_EXECUTOR = FileDownloadExecutors
            .newFixedThreadPool("ConnectionBlock");

    private boolean isResumeAvailableOnDB;
    private boolean acceptPartial;
    private boolean isChunked;

    private final AtomicBoolean alive;
    private volatile boolean paused;
    private volatile boolean error;
    private volatile Exception errorException;

    private String redirectedUrl;

    private DownloadLaunchRunnable(FileDownloadModel model, FileDownloadHeader header,
                                   IThreadPoolMonitor threadPoolMonitor,
                                   final int minIntervalMillis, int callbackProgressMaxCount,
                                   boolean isForceReDownload, boolean isWifiRequired,
                                   int maxRetryTimes) {
        this.alive = new AtomicBoolean(true);
        this.paused = false;
        this.isTriedFixRangeNotSatisfiable = false;

        this.model = model;
        this.userRequestHeader = header;
        this.isForceReDownload = isForceReDownload;
        this.isWifiRequired = isWifiRequired;
        this.database = CustomComponentHolder.getImpl().getDatabaseInstance();
        this.supportSeek = CustomComponentHolder.getImpl().isSupportSeek();
        this.threadPoolMonitor = threadPoolMonitor;
        this.validRetryTimes = maxRetryTimes;

        this.statusCallback = new DownloadStatusCallback(model,
                maxRetryTimes, minIntervalMillis, callbackProgressMaxCount);
    }

    private DownloadLaunchRunnable(DownloadStatusCallback callback, FileDownloadModel model,
                                   FileDownloadHeader header,
                                   IThreadPoolMonitor threadPoolMonitor,
                                   final int minIntervalMillis, int callbackProgressMaxCount,
                                   boolean isForceReDownload, boolean isWifiRequired,
                                   int maxRetryTimes) {
        this.alive = new AtomicBoolean(true);
        this.paused = false;
        this.isTriedFixRangeNotSatisfiable = false;

        this.model = model;
        this.userRequestHeader = header;
        this.isForceReDownload = isForceReDownload;
        this.isWifiRequired = isWifiRequired;
        this.database = CustomComponentHolder.getImpl().getDatabaseInstance();
        this.supportSeek = CustomComponentHolder.getImpl().isSupportSeek();
        this.threadPoolMonitor = threadPoolMonitor;
        this.validRetryTimes = maxRetryTimes;

        this.statusCallback = callback;
    }

    static DownloadLaunchRunnable createForTest(DownloadStatusCallback callback,
                                                FileDownloadModel model, FileDownloadHeader header,
                                                IThreadPoolMonitor threadPoolMonitor,
                                                final int minIntervalMillis,
                                                int callbackProgressMaxCount,
                                                boolean isForceReDownload, boolean isWifiRequired,
                                                int maxRetryTimes) {
        return new DownloadLaunchRunnable(callback, model, header, threadPoolMonitor,
                minIntervalMillis, callbackProgressMaxCount, isForceReDownload, isWifiRequired,
                maxRetryTimes);
    }

    public void pause() {
        this.paused = true;

        if (singleDownloadRunnable != null) singleDownloadRunnable.pause();
        @SuppressWarnings("unchecked") ArrayList<DownloadRunnable> pauseList =
                (ArrayList<DownloadRunnable>) downloadRunnableList.clone();
        for (DownloadRunnable runnable : pauseList) {
            if (runnable != null) {
                runnable.pause();
                // if runnable is null, then that one must be completed and removed
            }
        }
    }

    public void pending() {
        final List<ConnectionModel> connectionOnDBList = database
                .findConnectionModel(model.getId());
        //inspect model can be resumed or not, if false, the previous sofar cannot be used
        inspectTaskModelResumeAvailableOnDB(connectionOnDBList);
        statusCallback.onPending();
    }

    @Override
    public void run() {
        try {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

            // status checkout
            if (model.getStatus() != FileDownloadStatus.pending) {
                if (model.getStatus() == FileDownloadStatus.paused) {
                    if (FileDownloadLog.NEED_LOG) {
                        /**
                         * @see FileDownloadThreadPool#cancel(int), the invoking simultaneously
                         * with here. And this area is invoking before there, so,
                         * {@code cancel(int)} is fail.
                         *
                         * High concurrent cause.
                         */
                        FileDownloadLog.d(this, "High concurrent cause, start runnable but "
                                + "already paused %d", model.getId());
                    }

                } else {
                    onError(new RuntimeException(
                            FileDownloadUtils.formatString("Task[%d] can't start the download"
                                            + " runnable, because its status is %d not %d",
                                    model.getId(), model.getStatus(), FileDownloadStatus.pending)));
                }
                return;
            }

            if (!paused) {
                statusCallback.onStartThread();
            }

            do {
                if (paused) {
                    if (FileDownloadLog.NEED_LOG) {
                        /**
                         * @see FileDownloadThreadPool#cancel(int), the invoking simultaneously
                         * with here. And this area is invoking before there, so,
                         * {@code cancel(int)} is fail.
                         *
                         * High concurrent cause.
                         */
                        FileDownloadLog.d(this, "High concurrent cause, start runnable but "
                                + "already paused %d", model.getId());
                    }
                    return;
                }

                try {
                    // 1. check env state
                    checkupBeforeConnect();

                    // 2. trial connect
                    trialConnect();

                    // 3. reuse same task
                    checkupAfterGetFilename();

                    // 4. check local resume model
                    final List<ConnectionModel> connectionOnDBList = database
                            .findConnectionModel(model.getId());
                    inspectTaskModelResumeAvailableOnDB(connectionOnDBList);

                    if (paused) {
                        model.setStatus(FileDownloadStatus.paused);
                        return;
                    }

                    final long totalLength = model.getTotal();

                    // 5. pre-allocate if need.
                    handlePreAllocate(totalLength, model.getTempFilePath());

                    // 6. calculate block count
                    final int connectionCount = calcConnectionCount(totalLength);
                    if (connectionCount <= 0) {
                        throw new IllegalAccessException(FileDownloadUtils
                                .formatString("invalid connection count %d, the connection count"
                                        + " must be larger than 0", connectionCount));
                    }

                    if (totalLength == 0) {
                        return;
                    }

                    if (paused) {
                        model.setStatus(FileDownloadStatus.paused);
                        return;
                    }

                    // 7. start real connect and fetch to local filesystem
                    isSingleConnection = connectionCount == 1;
                    if (isSingleConnection) {
                        // single connection
                        realDownloadWithSingleConnection(totalLength);
                    } else {
                        // multiple connection
                        statusCallback.onMultiConnection();
                        if (isResumeAvailableOnDB) {
                            realDownloadWithMultiConnectionFromResume(connectionCount,
                                    connectionOnDBList);
                        } else {
                            realDownloadWithMultiConnectionFromBeginning(totalLength,
                                    connectionCount);
                        }
                    }

                } catch (IOException | IllegalAccessException
                        | InterruptedException | IllegalArgumentException
                        | FileDownloadSecurityException
                        | FileDownloadGiveUpRetryException e) {
                    if (isRetry(e)) {
                        onRetry(e);
                        continue;
                    } else {
                        onError(e);
                    }
                } catch (DiscardSafely discardSafely) {
                    return;
                } catch (RetryDirectly retryDirectly) {
                    model.setStatus(FileDownloadStatus.retry);
                    continue;
                }

                break;
            } while (true);
        } finally {
            statusCallback.discardAllMessage();

            if (paused) {
                statusCallback.onPausedDirectly();
            } else if (error) {
                statusCallback.onErrorDirectly(errorException);
            } else {
                try {
                    statusCallback.onCompletedDirectly();
                } catch (IOException e) {
                    statusCallback.onErrorDirectly(e);
                }
            }

            alive.set(false);
        }
    }

    private int calcConnectionCount(long totalLength) {
        if (isMultiConnectionAvailable()) {
            if (isResumeAvailableOnDB) {
                return model.getConnectionCount();
            } else {
                return CustomComponentHolder.getImpl()
                        .determineConnectionCount(model.getId(), model.getUrl(),
                                model.getPath(), totalLength);
            }
        } else {
            return 1;
        }
    }

    // the trial connection is for: 1. etag verify; 2. partial support verify.
    private void trialConnect() throws IOException, RetryDirectly, IllegalAccessException,
            FileDownloadSecurityException {
        FileDownloadConnection trialConnection = null;
        try {
            final ConnectionProfile trialConnectionProfile;
            if (isNeedForceDiscardRange) {
                trialConnectionProfile = ConnectionProfile.ConnectionProfileBuild
                        .buildTrialConnectionProfileNoRange();
            } else {
                trialConnectionProfile = ConnectionProfile.ConnectionProfileBuild
                        .buildTrialConnectionProfile();
            }
            final ConnectTask trialConnectTask = new ConnectTask.Builder()
                    .setDownloadId(model.getId())
                    .setUrl(model.getUrl())
                    .setEtag(model.getETag())
                    .setHeader(userRequestHeader)
                    .setConnectionProfile(trialConnectionProfile)
                    .build();
            trialConnection = trialConnectTask.connect();
            handleTrialConnectResult(trialConnectTask.getRequestHeader(),
                    trialConnectTask, trialConnection);

        } finally {
            if (trialConnection != null) trialConnection.ending();
        }
    }

    private boolean isMultiConnectionAvailable() {
        //noinspection SimplifiableIfStatement
        if (isResumeAvailableOnDB && model.getConnectionCount() <= 1) {
            return false;
        }

        return acceptPartial && supportSeek && !isChunked;
    }

    private int determineConnectionCount() {
        return defaultConnectionCount;
    }

    void inspectTaskModelResumeAvailableOnDB(List<ConnectionModel> connectionOnDBList) {
        // check resume available
        final long offset;
        final int connectionCount = model.getConnectionCount();
        final String tempFilePath = model.getTempFilePath();
        final String targetFilePath = model.getTargetFilePath();
        final boolean isMultiConnection = connectionCount > 1;
        if (isNeedForceDiscardRange) {
            offset = 0;
        } else if (isMultiConnection && !supportSeek) {
            // can't support seek for multi-connection is fatal problem, so discard resume.
            offset = 0;
        } else {
            final boolean resumeAvailable = FileDownloadUtils
                    .isBreakpointAvailable(model.getId(), model);
            if (resumeAvailable) {
                if (!supportSeek) {
                    offset = new File(tempFilePath).length();
                } else {
                    if (isMultiConnection) {
                        // when it is multi connections, the offset would be 0,
                        // because it only store on the connection table.
                        if (connectionCount != connectionOnDBList.size()) {
                            // dirty data
                            offset = 0;
                        } else {
                            offset = ConnectionModel.getTotalOffset(connectionOnDBList);
                        }
                    } else {
                        offset = model.getSoFar();
                    }
                }
            } else {
                offset = 0;
            }
        }

        model.setSoFar(offset);
        isResumeAvailableOnDB = offset > 0;
        if (!isResumeAvailableOnDB) {
            database.removeConnections(model.getId());
            FileDownloadUtils.deleteTaskFiles(targetFilePath, tempFilePath);
        }
    }

    private void handleTrialConnectResult(Map<String, List<String>> requestHeader,
                                          ConnectTask connectTask,
                                          FileDownloadConnection connection)
            throws IOException, RetryDirectly, IllegalArgumentException,
            FileDownloadSecurityException {
        final int id = model.getId();
        final int code = connection.getResponseCode();

        acceptPartial = FileDownloadUtils.isAcceptRange(code, connection);
        final boolean onlyFromBeginning = (code == HttpURLConnection.HTTP_OK
                || code == HttpURLConnection.HTTP_CREATED
                || code == FileDownloadConnection.NO_RESPONSE_CODE);
        final long totalLength = FileDownloadUtils.findInstanceLengthForTrial(connection);

        final String oldEtag = model.getETag();
        String newEtag = FileDownloadUtils.findEtag(id, connection);

        // handle whether need retry because of etag is overdue
        boolean isPreconditionFailed = false;
        do {
            if (code == HttpURLConnection.HTTP_PRECON_FAILED) {
                isPreconditionFailed = true;
                break;
            }

            if (oldEtag != null && !oldEtag.equals(newEtag)) {
                // etag changed.
                if (onlyFromBeginning || acceptPartial) {
                    // 200 or 206
                    isPreconditionFailed = true;
                    break;
                }
            }

            if (code == HttpURLConnection.HTTP_CREATED && connectTask.isRangeNotFromBeginning()) {
                // The request has been fulfilled and has resulted in one or more new resources
                // being created. mark this case is precondition failed for
                // 1. checkout whether accept partial
                // 2. 201 means new resources so range must be from beginning otherwise it can't
                // match local range.
                isPreconditionFailed = true;
                break;
            }

            if (code == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE) {
                if (acceptPartial && totalLength >= 0) {
                    // there is a special case: the server(such as Ali Cloud) doesn't follow the
                    // agreement(https://tools.ietf.org/html/rfc7233), so they return a 416
                    // response with Content-Range
                    FileDownloadLog.w(this,
                            "get 416 but the Content-Range is returned, no need to retry");
                    break;
                }
                if (model.getSoFar() > 0) {
                    // On the first connection range not satisfiable, there must something wrong,
                    // so have to retry.
                    isPreconditionFailed = true;
                    FileDownloadLog.w(this, "get 416, precondition failed and just retry");
                    break;
                } else {
                    // range is right, but get 416
                    if (!isNeedForceDiscardRange) {
                        // if range is still added, but range is right with 416 response, so we
                        // discard range on header and try again
                        isNeedForceDiscardRange = true;
                        isPreconditionFailed = true;
                        FileDownloadLog.w(this, "get 416, precondition failed and need "
                                + "to retry with discarding range");
                    }
                }
            }

        } while (false);

        if (isPreconditionFailed) {
            // the file on remote is changed
            if (isResumeAvailableOnDB) {
                FileDownloadLog.w(this, "there is precondition failed on this request[%d] "
                                + "with old etag[%s]、new etag[%s]、response code is %d",
                        id, oldEtag, newEtag, code);
            }

            database.removeConnections(model.getId());
            FileDownloadUtils.deleteTaskFiles(model.getTargetFilePath(), model.getTempFilePath());
            isResumeAvailableOnDB = false;

            if (oldEtag != null && oldEtag.equals(newEtag)) {
                FileDownloadLog.w(this, "the old etag[%s] is the same to the new etag[%s], "
                                + "but the response status code is %d not Partial(206), so wo have"
                                + " to start this task from very beginning for task[%d]!",
                        oldEtag, newEtag, code, id);
                newEtag = null;
            }

            model.setSoFar(0);
            model.setTotal(0);
            model.setETag(newEtag);
            model.resetConnectionCount();

            database.updateOldEtagOverdue(id, model.getETag(), model.getSoFar(), model.getTotal(),
                    model.getConnectionCount());

            // retry to check whether support partial or not.
            throw new RetryDirectly();
        }

        redirectedUrl = connectTask.getFinalRedirectedUrl();
        if (acceptPartial || onlyFromBeginning) {

            // update model
            String fileName = null;
            if (model.isPathAsDirectory()) {
                // filename
                fileName = FileDownloadUtils.findFilename(connection, model.getUrl());
            }
            isChunked = (totalLength == TOTAL_VALUE_IN_CHUNKED_RESOURCE);

            // callback
            statusCallback.onConnected(isResumeAvailableOnDB && acceptPartial,
                    totalLength, newEtag, fileName);

        } else {
            throw new FileDownloadHttpException(code,
                    requestHeader, connection.getResponseHeaderFields());
        }
    }

    private void realDownloadWithSingleConnection(final long totalLength)
            throws IOException, IllegalAccessException {

        // connect
        final ConnectionProfile profile;
        if (!acceptPartial) {
            model.setSoFar(0);
            profile = ConnectionProfile.ConnectionProfileBuild
                    .buildBeginToEndConnectionProfile(totalLength);
        } else {
            profile = ConnectionProfile.ConnectionProfileBuild
                    .buildToEndConnectionProfile(model.getSoFar(), model.getSoFar(),
                            totalLength - model.getSoFar());
        }

        singleDownloadRunnable = new DownloadRunnable.Builder()
                .setId(model.getId())
                .setConnectionIndex(-1)
                .setCallback(this)
                .setUrl(model.getUrl())
                .setEtag(model.getETag())
                .setHeader(userRequestHeader)
                .setWifiRequired(isWifiRequired)
                .setConnectionModel(profile)
                .setPath(model.getTempFilePath())
                .build();

        model.setConnectionCount(1);
        database.updateConnectionCount(model.getId(), 1);
        if (paused) {
            model.setStatus(FileDownloadStatus.paused);
            singleDownloadRunnable.pause();
        } else {
            singleDownloadRunnable.run();
        }
    }

    private void realDownloadWithMultiConnectionFromResume(final int connectionCount,
                                                           List<ConnectionModel> modelList)
            throws InterruptedException {
        if (connectionCount <= 1 || modelList.size() != connectionCount) {
            throw new IllegalArgumentException();
        }

        fetchWithMultipleConnection(modelList, model.getTotal());
    }

    private void realDownloadWithMultiConnectionFromBeginning(final long totalLength,
                                                              final int connectionCount)
            throws InterruptedException {
        long startOffset = 0;
        final long eachRegion = totalLength / connectionCount;
        final int id = model.getId();

        final List<ConnectionModel> connectionModelList = new ArrayList<>();

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

            final long endOffset;
            if (i == connectionCount - 1) {
                // avoid float precision error
                endOffset = ConnectionProfile.RANGE_INFINITE;
            } else {
                // [startOffset, endOffset)
                endOffset = startOffset + eachRegion - 1;
            }

            final ConnectionModel connectionModel = new ConnectionModel();
            connectionModel.setId(id);
            connectionModel.setIndex(i);
            connectionModel.setStartOffset(startOffset);
            connectionModel.setCurrentOffset(startOffset);
            connectionModel.setEndOffset(endOffset);
            connectionModelList.add(connectionModel);

            database.insertConnectionModel(connectionModel);
            startOffset += eachRegion;
        }

        model.setConnectionCount(connectionCount);
        database.updateConnectionCount(id, connectionCount);

        fetchWithMultipleConnection(connectionModelList, totalLength);
    }

    private void fetchWithMultipleConnection(final List<ConnectionModel> connectionModelList,
                                             final long totalLength) throws InterruptedException {
        final int id = model.getId();
        final String etag = model.getETag();
        final String url = redirectedUrl != null ? redirectedUrl : model.getUrl();
        final String path = model.getTempFilePath();

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this,
                    "fetch data with multiple connection(count: [%d]) for task[%d] totalLength[%d]",
                    connectionModelList.size(), id, totalLength);
        }

        long totalOffset = 0;

        // why not with etag when not resume from the database? because do this can avoid
        // precondition failed on separate downloading.
        final boolean withEtag = isResumeAvailableOnDB;
        for (ConnectionModel connectionModel : connectionModelList) {
            final long contentLength;
            if (connectionModel.getEndOffset() == ConnectionProfile.RANGE_INFINITE) {
                // must be the last one
                contentLength = totalLength - connectionModel.getCurrentOffset();
            } else {
                contentLength = connectionModel.getEndOffset() - connectionModel
                        .getCurrentOffset() + 1;
            }

            totalOffset += (connectionModel.getCurrentOffset() - connectionModel.getStartOffset());

            if (contentLength == 0) {
                // [start, end), offset contain the start one, so need - 1.
                // it has already done, so pass.
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(this, "pass connection[%d-%d], because it has been completed",
                            connectionModel.getId(), connectionModel.getIndex());
                }
                continue;
            }

            final DownloadRunnable.Builder builder = new DownloadRunnable.Builder();

            final ConnectionProfile connectionProfile = ConnectionProfile.ConnectionProfileBuild
                    .buildConnectionProfile(
                            connectionModel.getStartOffset(), connectionModel.getCurrentOffset(),
                            connectionModel.getEndOffset(), contentLength);

            final DownloadRunnable runnable = builder
                    .setId(id)
                    .setConnectionIndex(connectionModel.getIndex())
                    .setCallback(this)
                    .setUrl(url)
                    .setEtag(withEtag ? etag : null)
                    .setHeader(userRequestHeader)
                    .setWifiRequired(isWifiRequired)
                    .setConnectionModel(connectionProfile)
                    .setPath(path)
                    .build();

            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "enable multiple connection: %s", connectionModel);
            }

            if (runnable == null) {
                throw new IllegalArgumentException("the download runnable must not be null!");
            }

            downloadRunnableList.add(runnable);
        }

        if (totalOffset != model.getSoFar()) {
            FileDownloadLog.w(this, "correct the sofar[%d] from connection table[%d]",
                    model.getSoFar(), totalOffset);
            model.setSoFar(totalOffset);
        }

        List<Callable<Object>> subTasks = new ArrayList<>(downloadRunnableList.size());
        for (DownloadRunnable runnable : downloadRunnableList) {
            if (paused) {
                runnable.pause();
                continue;
            }
            subTasks.add(Executors.callable(runnable));
        }
        if (paused) {
            model.setStatus(FileDownloadStatus.paused);
            return;
        }

        List<Future<Object>> subTaskFutures = DOWNLOAD_EXECUTOR.invokeAll(subTasks);
        if (FileDownloadLog.NEED_LOG) {
            for (Future<Object> future : subTaskFutures) {
                FileDownloadLog.d(this, "finish sub-task for [%d] %B %B",
                        id, future.isDone(), future.isCancelled());
            }
        }
    }

    private void handlePreAllocate(long totalLength, String path)
            throws IOException, IllegalAccessException {

        FileDownloadOutputStream outputStream = null;
        try {

            if (totalLength != TOTAL_VALUE_IN_CHUNKED_RESOURCE) {
                outputStream = FileDownloadUtils.createOutputStream(model.getTempFilePath());
                final long breakpointBytes = new File(path).length();
                final long requiredSpaceBytes = totalLength - breakpointBytes;

                final long freeSpaceBytes = FileDownloadUtils.getFreeSpaceBytes(path);

                if (freeSpaceBytes < requiredSpaceBytes) {
                    // throw a out of space exception.
                    throw new FileDownloadOutOfSpaceException(freeSpaceBytes,
                            requiredSpaceBytes, breakpointBytes);
                } else if (!FileDownloadProperties.getImpl().fileNonPreAllocation) {
                    // pre allocate.
                    outputStream.setLength(totalLength);
                }
            }
        } finally {
            if (outputStream != null) outputStream.close();
        }
    }

    private long lastCallbackBytes = 0;
    private long lastCallbackTimestamp = 0;

    private long lastUpdateBytes = 0;
    private long lastUpdateTimestamp = 0;

    @Override
    public void onProgress(long increaseBytes) {
        if (paused) return;

        statusCallback.onProgress(increaseBytes);
    }

    @Override
    public void onCompleted(DownloadRunnable doneRunnable, long startOffset, long endOffset) {
        if (paused) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "the task[%d] has already been paused, so pass the"
                        + " completed callback", model.getId());
            }
            return;
        }

        final int doneConnectionIndex = doneRunnable.connectionIndex;
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "the connection has been completed(%d): [%d, %d)  %d",
                    doneConnectionIndex, startOffset, endOffset, model.getTotal());
        }

        if (isSingleConnection) {
            if (startOffset != 0 && endOffset != model.getTotal()) {
                FileDownloadLog.e(this, "the single task not completed corrected(%d, %d != %d) "
                        + "for task(%d)", startOffset, endOffset, model.getTotal(), model.getId());
            }
        } else {
            synchronized (downloadRunnableList) {
                downloadRunnableList.remove(doneRunnable);
            }
        }
    }

    @Override
    public boolean isRetry(Exception exception) {
        if (exception instanceof FileDownloadHttpException) {
            final FileDownloadHttpException httpException = (FileDownloadHttpException) exception;

            final int code = httpException.getCode();

            if (isSingleConnection && code == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE) {
                if (!isTriedFixRangeNotSatisfiable) {
                    FileDownloadUtils
                            .deleteTaskFiles(model.getTargetFilePath(), model.getTempFilePath());
                    isTriedFixRangeNotSatisfiable = true;
                    return true;
                }
            }
        }

        return validRetryTimes > 0 && !(exception instanceof FileDownloadGiveUpRetryException);
    }

    @Override
    public void onError(Exception exception) {
        error = true;
        errorException = exception;

        if (paused) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "the task[%d] has already been paused, so pass the"
                        + " error callback", model.getId());
            }
            return;
        }

        // discard all
        @SuppressWarnings("unchecked") ArrayList<DownloadRunnable> discardList =
                (ArrayList<DownloadRunnable>) downloadRunnableList.clone();
        for (DownloadRunnable runnable : discardList) {
            if (runnable != null) {
                runnable.discard();
                // if runnable is null, then that one must be completed and removed
            }
        }
    }

    @Override
    public void onRetry(Exception exception) {
        if (paused) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "the task[%d] has already been paused, so pass the"
                        + " retry callback", model.getId());
            }
            return;
        }

        if (validRetryTimes-- < 0) {
            FileDownloadLog.e(this, "valid retry times is less than 0(%d) for download task(%d)",
                    validRetryTimes, model.getId());
        }

        statusCallback.onRetry(exception, validRetryTimes);
    }

    @Override
    public void syncProgressFromCache() {
        database.updateProgress(model.getId(), model.getSoFar());
    }

    private void checkupBeforeConnect()
            throws FileDownloadGiveUpRetryException {

        // 1. check whether need access-network-state permission?
        if (isWifiRequired
                && !FileDownloadUtils.checkPermission(Manifest.permission.ACCESS_NETWORK_STATE)) {
            throw new FileDownloadGiveUpRetryException(
                    FileDownloadUtils.formatString("Task[%d] can't start the download runnable,"
                                    + " because this task require wifi, but user application "
                                    + "nor current process has %s, so we can't check whether "
                                    + "the network type connection.", model.getId(),
                            Manifest.permission.ACCESS_NETWORK_STATE));
        }

        // 2. check whether need wifi to download?
        if (isWifiRequired && FileDownloadUtils.isNetworkNotOnWifiType()) {
            throw new FileDownloadNetworkPolicyException();
        }
    }

    private void checkupAfterGetFilename() throws RetryDirectly, DiscardSafely {
        final int id = model.getId();

        if (model.isPathAsDirectory()) {
            // this scope for caring about the case of there is another task is provided
            // the same path to store file and the same url.

            final String targetFilePath = model.getTargetFilePath();

            // get the ID after got the filename.
            final int fileCaseId = FileDownloadUtils.generateId(model.getUrl(),
                    targetFilePath);

            // whether the file with the filename has been existed.
            if (FileDownloadHelper.inspectAndInflowDownloaded(id,
                    targetFilePath, isForceReDownload, false)) {
                database.remove(id);
                database.removeConnections(id);
                throw new DiscardSafely();
            }

            final FileDownloadModel fileCaseModel = database.find(fileCaseId);

            if (fileCaseModel != null) {
                // the task with the same file name and url has been exist.

                // whether the another task with the same file and url is downloading.
                if (FileDownloadHelper.inspectAndInflowDownloading(id, fileCaseModel,
                        threadPoolMonitor, false)) {
                    //it has been post to upper layer the 'warn' message, so the current
                    // task no need to continue download.
                    database.remove(id);
                    database.removeConnections(id);
                    throw new DiscardSafely();
                }

                final List<ConnectionModel> connectionModelList = database
                        .findConnectionModel(fileCaseId);

                // the another task with the same file name and url is paused
                database.remove(fileCaseId);
                database.removeConnections(fileCaseId);
                FileDownloadUtils.deleteTargetFile(model.getTargetFilePath());

                if (FileDownloadUtils.isBreakpointAvailable(fileCaseId, fileCaseModel)) {
                    model.setSoFar(fileCaseModel.getSoFar());
                    model.setTotal(fileCaseModel.getTotal());
                    model.setETag(fileCaseModel.getETag());
                    model.setConnectionCount(fileCaseModel.getConnectionCount());
                    database.update(model);

                    // re connect to resume from breakpoint.
                    if (connectionModelList != null) {
                        for (ConnectionModel connectionModel : connectionModelList) {
                            connectionModel.setId(id);
                            database.insertConnectionModel(connectionModel);
                        }
                    }

                    // retry
                    throw new RetryDirectly();
                }
            }

            // whether there is an another running task with the same target-file-path.
            if (FileDownloadHelper.inspectAndInflowConflictPath(id, model.getSoFar(),
                    model.getTempFilePath(),
                    targetFilePath,
                    threadPoolMonitor)) {
                database.remove(id);
                database.removeConnections(id);

                throw new DiscardSafely();
            }
        }
    }

    public int getId() {
        return model.getId();
    }

    public boolean isAlive() {
        return alive.get() || this.statusCallback.isAlive();
    }

    public String getTempFilePath() {
        return model.getTempFilePath();
    }

    class RetryDirectly extends Throwable {
    }

    class DiscardSafely extends Throwable {
    }

    public static class Builder {
        private FileDownloadModel model;
        private FileDownloadHeader header;
        private IThreadPoolMonitor threadPoolMonitor;
        private Integer minIntervalMillis;
        private Integer callbackProgressMaxCount;
        private Boolean isForceReDownload;
        private Boolean isWifiRequired;
        private Integer maxRetryTimes;

        public Builder setModel(FileDownloadModel model) {
            this.model = model;
            return this;
        }

        public Builder setHeader(FileDownloadHeader header) {
            this.header = header;
            return this;
        }

        public Builder setThreadPoolMonitor(IThreadPoolMonitor threadPoolMonitor) {
            this.threadPoolMonitor = threadPoolMonitor;
            return this;
        }

        public Builder setMinIntervalMillis(Integer minIntervalMillis) {
            this.minIntervalMillis = minIntervalMillis;
            return this;
        }

        public Builder setCallbackProgressMaxCount(Integer callbackProgressMaxCount) {
            this.callbackProgressMaxCount = callbackProgressMaxCount;
            return this;
        }

        public Builder setForceReDownload(Boolean forceReDownload) {
            isForceReDownload = forceReDownload;
            return this;
        }

        public Builder setWifiRequired(Boolean wifiRequired) {
            isWifiRequired = wifiRequired;
            return this;
        }

        public Builder setMaxRetryTimes(Integer maxRetryTimes) {
            this.maxRetryTimes = maxRetryTimes;
            return this;
        }

        public DownloadLaunchRunnable build() {
            if (model == null || threadPoolMonitor == null
                    || minIntervalMillis == null || callbackProgressMaxCount == null
                    || isForceReDownload == null || isWifiRequired == null
                    || maxRetryTimes == null) {
                throw new IllegalArgumentException();
            }

            return new DownloadLaunchRunnable(model, header, threadPoolMonitor,
                    minIntervalMillis, callbackProgressMaxCount,
                    isForceReDownload, isWifiRequired, maxRetryTimes);
        }
    }
}


/*
 * Copyright (c) 2015 LingoChamp Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.liulishuo.filedownloader.services;

import android.content.Intent;

import com.liulishuo.filedownloader.model.FileDownloadModel;
import com.liulishuo.filedownloader.model.FileDownloadStatus;
import com.liulishuo.filedownloader.util.FileDownloadHelper;
import com.liulishuo.filedownloader.util.FileDownloadUtils;

/**
 * The handler broadcast from filedownloader.
 */
public class FileDownloadBroadcastHandler {
    public static final String ACTION_COMPLETED = "filedownloader.intent.action.completed";
    public static final String KEY_MODEL = "model";

    /**
     * Parse the {@code intent} from the filedownloader broadcast.
     *
     * @param intent the intent from the broadcast.
     * @return the file download model.
     */
    public static FileDownloadModel parseIntent(Intent intent) {
        if (!ACTION_COMPLETED.equals(intent.getAction())) {
            throw new IllegalArgumentException(FileDownloadUtils.
                    formatString("can't recognize the intent with action %s, on the current"
                            + " version we only support action [%s]",
                            intent.getAction(), ACTION_COMPLETED));
        }

        return intent.getParcelableExtra(KEY_MODEL);
    }

    public static void sendCompletedBroadcast(FileDownloadModel model) {
        if (model == null) throw new IllegalArgumentException();
        if (model.getStatus() != FileDownloadStatus.completed) throw new IllegalStateException();

        final Intent intent = new Intent(ACTION_COMPLETED);
        intent.putExtra(KEY_MODEL, model);

        FileDownloadHelper.getAppContext().sendBroadcast(intent);
    }
}


/*
 * Copyright (c) 2015 LingoChamp Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.liulishuo.filedownloader;

import android.app.Notification;

/**
 * The FileDownloader asynchronous line.
 */

public class FileDownloadLineAsync {

    /**
     * The {@link FileDownloader#startForeground(int, Notification)} request.
     */
    public boolean startForeground(final int id, final Notification notification) {
        if (FileDownloader.getImpl().isServiceConnected()) {
            FileDownloader.getImpl().startForeground(id, notification);
            return true;
        } else {
            FileDownloader.getImpl().bindService(new Runnable() {
                @Override
                public void run() {
                    FileDownloader.getImpl().startForeground(id, notification);
                }
            });
            return false;
        }
    }

}


/*
 * Copyright (c) 2015 LingoChamp Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.liulishuo.filedownloader.connection;

import com.liulishuo.filedownloader.util.FileDownloadHelper;

/**
 * The default connection count adapter.
 */

public class DefaultConnectionCountAdapter implements FileDownloadHelper.ConnectionCountAdapter {

    // 1 connection: [0, 1MB)
    private static final long ONE_CONNECTION_UPPER_LIMIT = 1024 * 1024; // 1MB
    // 2 connection: [1MB, 5MB)
    private static final long TWO_CONNECTION_UPPER_LIMIT = 5 * 1024 * 1024; // 5MB
    // 3 connection: [5MB, 50MB)
    private static final long THREE_CONNECTION_UPPER_LIMIT = 50 * 1024 * 1024; // 50MB
    // 4 connection: [50MB, 100MB)
    private static final long FOUR_CONNECTION_UPPER_LIMIT = 100 * 1024 * 1024; // 100MB

    @Override
    public int determineConnectionCount(int downloadId, String url, String path, long totalLength) {
        if (totalLength < ONE_CONNECTION_UPPER_LIMIT) {
            return 1;
        }

        if (totalLength < TWO_CONNECTION_UPPER_LIMIT) {
            return 2;
        }

        if (totalLength < THREE_CONNECTION_UPPER_LIMIT) {
            return 3;
        }

        if (totalLength < FOUR_CONNECTION_UPPER_LIMIT) {
            return 4;
        }

        return 5;
    }
}


/*
 * Copyright (c) 2015 LingoChamp Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.liulishuo.filedownloader;

import android.app.Notification;
import android.os.Looper;

import com.liulishuo.filedownloader.model.FileDownloadStatus;

import java.io.File;

/**
 * The FileDownload synchronous line.
 *
 * @see FileDownloader#insureServiceBind()
 * @see #wait(ConnectSubscriber)
 */

public class FileDownloadLine {

    /**
     * The {@link FileDownloader#startForeground(int, Notification)} request.
     */
    public void startForeground(final int id, final Notification notification) {
        if (FileDownloader.getImpl().isServiceConnected()) {
            FileDownloader.getImpl().startForeground(id, notification);
            return;
        }

        final ConnectSubscriber subscriber = new ConnectSubscriber() {
            @Override
            public void connected() {
                FileDownloader.getImpl().startForeground(id, notification);
            }

            @Override
            public Object getValue() {
                return null;
            }
        };

        wait(subscriber);
    }

    /**
     * The {@link FileDownloader#getSoFar(int)} request.
     */
    public long getSoFar(final int id) {
        if (FileDownloader.getImpl().isServiceConnected()) {
            return FileDownloader.getImpl().getSoFar(id);
        }

        final ConnectSubscriber subscriber = new ConnectSubscriber() {
            private long mValue;

            @Override
            public void connected() {
                mValue = FileDownloader.getImpl().getSoFar(id);
            }

            @Override
            public Object getValue() {
                return mValue;
            }
        };

        wait(subscriber);

        return (long) subscriber.getValue();
    }

    /**
     * The {@link FileDownloader#getTotal(int)} request.
     */
    public long getTotal(final int id) {
        if (FileDownloader.getImpl().isServiceConnected()) {
            return FileDownloader.getImpl().getTotal(id);
        }

        final ConnectSubscriber subscriber = new ConnectSubscriber() {
            private long mValue;

            @Override
            public void connected() {
                mValue = FileDownloader.getImpl().getTotal(id);
            }

            @Override
            public Object getValue() {
                return mValue;
            }
        };

        wait(subscriber);

        return (long) subscriber.getValue();
    }

    /**
     * The {@link FileDownloader#getStatus(int, String)} request.
     */
    public byte getStatus(final int id, final String path) {
        if (FileDownloader.getImpl().isServiceConnected()) {
            return FileDownloader.getImpl().getStatus(id, path);
        }

        if (path != null && new File(path).exists()) {
            return FileDownloadStatus.completed;
        }

        final ConnectSubscriber subscriber = new ConnectSubscriber() {
            private byte mValue;

            @Override
            public void connected() {
                mValue = FileDownloader.getImpl().getStatus(id, path);
            }

            @Override
            public Object getValue() {
                return mValue;
            }
        };

        wait(subscriber);

        return (byte) subscriber.getValue();
    }

    private void wait(final ConnectSubscriber subscriber) {
        final ConnectListener connectListener = new ConnectListener(subscriber);

        //noinspection SynchronizationOnLocalVariableOrMethodParameter
        synchronized (connectListener) {
            FileDownloader.getImpl().bindService(connectListener);

            if (!connectListener.isFinished()) {

                if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
                    throw new IllegalThreadStateException("Sorry, FileDownloader can not block the "
                            + "main thread, because the system is also  callbacks "
                            + "ServiceConnection#onServiceConnected method in the main thread.");
                }

                try {
                    connectListener.wait(200 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class ConnectListener implements Runnable {
        private boolean mIsFinished = false;
        private final ConnectSubscriber mSubscriber;

        ConnectListener(ConnectSubscriber subscriber) {
            this.mSubscriber = subscriber;
        }

        public boolean isFinished() {
            return mIsFinished;
        }

        @Override
        public void run() {
            synchronized (this) {
                mSubscriber.connected();
                mIsFinished = true;
                notifyAll();
            }
        }
    }

    interface ConnectSubscriber {
        void connected();

        Object getValue();
    }
}


/*
 * Copyright (c) 2015 LingoChamp Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.liulishuo.filedownloader.model;

import android.os.Parcel;
import android.os.Parcelable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * We have already handled Etag internal for guaranteeing tasks resuming from the breakpoint, in
 * other words, if the task has downloaded and got Etag, we will add the 'If-Match' and the 'Range'
 * K-V to its request header automatically.
 */
public class FileDownloadHeader implements Parcelable {

    private HashMap<String, List<String>> mHeaderMap;

    /**
     * We have already handled etag, and will add 'If-Match' & 'Range' value if it works.
     *
     * @see com.liulishuo.filedownloader.download.ConnectTask#addUserRequiredHeader
     */
    public void add(String name, String value) {
        if (name == null) throw new NullPointerException("name == null");
        if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
        if (value == null) throw new NullPointerException("value == null");

        if (mHeaderMap == null) {
            mHeaderMap = new HashMap<>();
        }

        List<String> values = mHeaderMap.get(name);
        if (values == null) {
            values = new ArrayList<>();
            mHeaderMap.put(name, values);
        }

        if (!values.contains(value)) {
            values.add(value);
        }
    }

    /**
     * We have already handled etag, and will add 'If-Match' & 'Range' value if it works.
     *
     * @see com.liulishuo.filedownloader.download.ConnectTask#addUserRequiredHeader
     */
    public void add(String line) {
        String[] parsed = line.split(":");
        final String name = parsed[0].trim();
        final String value = parsed[1].trim();

        add(name, value);
    }

    /**
     * Remove all files with the name.
     */
    public void removeAll(String name) {
        if (mHeaderMap == null) {
            return;
        }

        mHeaderMap.remove(name);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeMap(mHeaderMap);
    }

    public HashMap<String, List<String>> getHeaders() {
        return mHeaderMap;
    }

    public FileDownloadHeader() {
    }

    protected FileDownloadHeader(Parcel in) {
        //noinspection unchecked
        this.mHeaderMap = in.readHashMap(String.class.getClassLoader());
    }

    public static final Creator<FileDownloadHeader> CREATOR = new Creator<FileDownloadHeader>() {
        public FileDownloadHeader createFromParcel(Parcel source) {
            return new FileDownloadHeader(source);
        }

        public FileDownloadHeader[] newArray(int size) {
            return new FileDownloadHeader[size];
        }
    };

    @Override
    public String toString() {
        return mHeaderMap.toString();
    }
}


/*
 * Copyright (c) 2015 LingoChamp Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.liulishuo.filedownloader;

import android.app.Application;
import android.app.Notification;
import android.content.ComponentName;
import android.content.Context;
import android.content.ServiceConnection;
import android.os.IBinder;

import com.liulishuo.filedownloader.download.CustomComponentHolder;
import com.liulishuo.filedownloader.event.DownloadServiceConnectChangedEvent;
import com.liulishuo.filedownloader.model.FileDownloadStatus;
import com.liulishuo.filedownloader.model.FileDownloadTaskAtom;
import com.liulishuo.filedownloader.services.DownloadMgrInitialParams;
import com.liulishuo.filedownloader.util.FileDownloadHelper;
import com.liulishuo.filedownloader.util.FileDownloadLog;
import com.liulishuo.filedownloader.util.FileDownloadProperties;
import com.liulishuo.filedownloader.util.FileDownloadUtils;

import java.io.File;
import java.util.List;

/**
 * The basic entrance for FileDownloader.
 *
 * @see com.liulishuo.filedownloader.services.FileDownloadService The service for FileDownloader.
 * @see FileDownloadProperties
 */
@SuppressWarnings("WeakerAccess")
public class FileDownloader {

    /**
     * You can invoke this method anytime before you using the FileDownloader.
     *

     * If you want to register your own customize components please using
     * {@link #setupOnApplicationOnCreate(Application)} on the {@link Application#onCreate()}
     * instead.
     *
     * @param context the context of Application or Activity etc..
     */
    public static void setup(Context context) {
        FileDownloadHelper.holdContext(context.getApplicationContext());
    }

    /**
     * Using this method to setup the FileDownloader only you want to register your own customize
     * components for Filedownloader, otherwise just using {@link #setup(Context)} instead.
     *

     * Please invoke this method on the {@link Application#onCreate()} because of the customize
     * components must be assigned before FileDownloader is running.
     *

     * Such as:
     *

     * class MyApplication extends Application {
     *     ...
     *     public void onCreate() {
     *          ...
     *          FileDownloader.setupOnApplicationOnCreate(this)
     *              .idGenerator(new MyIdGenerator())
     *              .database(new MyDatabase())
     *              ...
     *              .commit();
     *          ...
     *     }
     *     ...
     * }
     * @param application the application.
     * @return the customize components maker.
     */
    public static DownloadMgrInitialParams.InitCustomMaker setupOnApplicationOnCreate(
            Application application) {
        final Context context = application.getApplicationContext();
        FileDownloadHelper.holdContext(context);

        DownloadMgrInitialParams.InitCustomMaker customMaker =
                new DownloadMgrInitialParams.InitCustomMaker();
        CustomComponentHolder.getImpl().setInitCustomMaker(customMaker);

        return customMaker;
    }

    /**
     * @deprecated please use {@link #setup(Context)} instead.
     */
    public static void init(final Context context) {
        if (context == null) {
            throw new IllegalArgumentException("the provided context must not be null!");
        }

        setup(context);
    }

    /**
     * @deprecated please using {@link #setupOnApplicationOnCreate(Application)} instead.
     */
    public static void init(final Context context,
                            final DownloadMgrInitialParams.InitCustomMaker maker) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(FileDownloader.class, "init Downloader with params: %s %s",
                    context, maker);
        }

        if (context == null) {
            throw new IllegalArgumentException("the provided context must not be null!");
        }

        FileDownloadHelper.holdContext(context.getApplicationContext());

        CustomComponentHolder.getImpl().setInitCustomMaker(maker);
    }

    private static final class HolderClass {
        private static final FileDownloader INSTANCE = new FileDownloader();
    }

    public static FileDownloader getImpl() {
        return HolderClass.INSTANCE;
    }

    /**
     * For avoiding missing screen frames.
     *

     * This mechanism is used for avoid methods in {@link FileDownloadListener} is invoked too
     * frequent in result the system missing screen frames in the main thread.
     *

     * We wrap the message package which size is {@link FileDownloadMessageStation#SUB_PACKAGE_SIZE}
     * and post the package to the main thread with the interval:
     * {@link FileDownloadMessageStation#INTERVAL} milliseconds.
     *

     * The default interval is 10ms, if {@code intervalMillisecond} equal to or less than 0, each
     * callback in {@link FileDownloadListener} will be posted to the main thread immediately.
     *
     * @param intervalMillisecond The time interval between posting two message packages.
     * @see #enableAvoidDropFrame()
     * @see #disableAvoidDropFrame()
     * @see #setGlobalHandleSubPackageSize(int)
     */
    public static void setGlobalPost2UIInterval(final int intervalMillisecond) {
        FileDownloadMessageStation.INTERVAL = intervalMillisecond;
    }

    /**
     * For avoiding missing screen frames.
     *

     * This mechanism is used for avoid methods in {@link FileDownloadListener} is invoked too
     * frequent in result the system missing screen frames in the main thread.
     *

     * We wrap the message package which size is {@link FileDownloadMessageStation#SUB_PACKAGE_SIZE}
     * and post the package to the main thread with the interval:
     * {@link FileDownloadMessageStation#INTERVAL} milliseconds.
     *

     * The default count of message for a message package is 5.
     *
     * @param packageSize The count of message for a message package.
     * @see #setGlobalPost2UIInterval(int)
     */
    public static void setGlobalHandleSubPackageSize(final int packageSize) {
        if (packageSize <= 0) {
            throw new IllegalArgumentException("sub package size must more than 0");
        }
        FileDownloadMessageStation.SUB_PACKAGE_SIZE = packageSize;
    }

    /**
     * Avoid missing screen frames, this leads to all callbacks in {@link FileDownloadListener} do
     * not be invoked at once when it has already achieved to ensure callbacks don't be too frequent
     *
     * @see #isEnabledAvoidDropFrame()
     * @see #setGlobalPost2UIInterval(int)
     */
    public static void enableAvoidDropFrame() {
        setGlobalPost2UIInterval(FileDownloadMessageStation.DEFAULT_INTERVAL);
    }

    /**
     * Disable avoiding missing screen frames, let all callbacks in {@link FileDownloadListener}
     * can be invoked at once when it achieve.
     *
     * @see #isEnabledAvoidDropFrame()
     * @see #setGlobalPost2UIInterval(int)
     */
    public static void disableAvoidDropFrame() {
        setGlobalPost2UIInterval(-1);
    }

    /**
     * @return {@code true} if enabled the function of avoiding missing screen frames.
     * @see #enableAvoidDropFrame()
     * @see #disableAvoidDropFrame()
     * @see #setGlobalPost2UIInterval(int)
     */
    public static boolean isEnabledAvoidDropFrame() {
        return FileDownloadMessageStation.isIntervalValid();
    }

    /**
     * Create a download task.
     */
    public BaseDownloadTask create(final String url) {
        return new DownloadTask(url);
    }

    /**
     * Start the download queue by the same listener.
     *
     * @param listener Used to assemble tasks which is bound by the same {@code listener}
     * @param isSerial Whether start tasks one by one rather than parallel.
     * @return {@code true} if start tasks successfully.
     */
    public boolean start(final FileDownloadListener listener, final boolean isSerial) {

        if (listener == null) {
            FileDownloadLog.w(this, "Tasks with the listener can't start, because the listener "
                    + "provided is null: [null, %B]", isSerial);
            return false;
        }

        return isSerial
                ? getQueuesHandler().startQueueSerial(listener)
                : getQueuesHandler().startQueueParallel(listener);
    }

    /**
     * Pause the download queue by the same {@code listener}.
     *
     * @param listener the listener.
     * @see #pause(int)
     */
    public void pause(final FileDownloadListener listener) {
        FileDownloadTaskLauncher.getImpl().expire(listener);
        final List<BaseDownloadTask.IRunningTask> taskList =
                FileDownloadList.getImpl().copy(listener);
        for (BaseDownloadTask.IRunningTask task : taskList) {
            task.getOrigin().pause();
        }
    }

    /**
     * Pause all tasks running in FileDownloader.
     */
    public void pauseAll() {
        FileDownloadTaskLauncher.getImpl().expireAll();
        final BaseDownloadTask.IRunningTask[] downloadList = FileDownloadList.getImpl().copy();
        for (BaseDownloadTask.IRunningTask task : downloadList) {
            task.getOrigin().pause();
        }
        // double check, for case: File Download progress alive but ui progress has died and relived
        // so FileDownloadList not always contain all running task exactly.
        if (FileDownloadServiceProxy.getImpl().isConnected()) {
            FileDownloadServiceProxy.getImpl().pauseAllTasks();
        } else {
            PauseAllMarker.createMarker();
        }
    }

    /**
     * Pause downloading tasks with the {@code id}.
     *
     * @param id the {@code id} .
     * @return The size of tasks has been paused.
     * @see #pause(FileDownloadListener)
     */
    public int pause(final int id) {
        List<BaseDownloadTask.IRunningTask> taskList = FileDownloadList.getImpl()
                .getDownloadingList(id);
        if (null == taskList || taskList.isEmpty()) {
            FileDownloadLog.w(this, "request pause but not exist %d", id);
            return 0;
        }

        for (BaseDownloadTask.IRunningTask task : taskList) {
            task.getOrigin().pause();
        }

        return taskList.size();
    }

    /**
     * Clear the data with the provided {@code id}.
     * Normally used to deleting the data in filedownloader database, when it is paused or in
     * downloading status. If you want to re-download it clearly.
     *

     * Note: YOU NO NEED to clear the data when it is already completed downloading
     * because the data would be deleted when it completed downloading automatically by
     * FileDownloader.
     *

     * If there are tasks with the {@code id} in downloading, will be paused first;
     * If delete the data with the {@code id} in the filedownloader database successfully, will try
     * to delete its intermediate downloading file and downloaded file.
     *
     * @param id             the download {@code id}.
     * @param targetFilePath the target path.
     * @return {@code true} if the data with the {@code id} in filedownloader database was deleted,
     * and tasks with the {@code id} was paused; {@code false} otherwise.
     */
    public boolean clear(final int id, final String targetFilePath) {
        pause(id);

        if (FileDownloadServiceProxy.getImpl().clearTaskData(id)) {
            // delete the task data in the filedownloader database successfully or no data with the
            // id in filedownloader database.
            final File intermediateFile = new File(FileDownloadUtils.getTempPath(targetFilePath));
            if (intermediateFile.exists()) {
                //noinspection ResultOfMethodCallIgnored
                intermediateFile.delete();
            }

            final File targetFile = new File(targetFilePath);
            if (targetFile.exists()) {
                //noinspection ResultOfMethodCallIgnored
                targetFile.delete();
            }

            return true;
        }

        return false;
    }

    /**
     * Clear all data in the filedownloader database.
     *

     * Note: Normally, YOU NO NEED to clearAllTaskData manually, because the
     * FileDownloader will maintain those data to ensure only if the data available for resuming
     * can be kept automatically.
     *
     * @see #clear(int, String)
     */
    public void clearAllTaskData() {
        pauseAll();

        FileDownloadServiceProxy.getImpl().clearAllTaskData();
    }

    /**
     * Get downloaded bytes so far by the downloadId.
     */
    public long getSoFar(final int downloadId) {
        BaseDownloadTask.IRunningTask task = FileDownloadList.getImpl().get(downloadId);
        if (task == null) {
            return FileDownloadServiceProxy.getImpl().getSofar(downloadId);
        }

        return task.getOrigin().getLargeFileSoFarBytes();
    }

    /**
     * Get the total bytes of the target file for the task with the {code id}.
     */
    public long getTotal(final int id) {
        BaseDownloadTask.IRunningTask task = FileDownloadList.getImpl().get(id);
        if (task == null) {
            return FileDownloadServiceProxy.getImpl().getTotal(id);
        }

        return task.getOrigin().getLargeFileTotalBytes();
    }

    /**
     * @param id The downloadId.
     * @return The downloading status without cover the completed status (if completed you will
     * receive
     * {@link FileDownloadStatus#INVALID_STATUS} ).
     * @see #getStatus(String, String)
     * @see #getStatus(int, String)
     */
    public byte getStatusIgnoreCompleted(final int id) {
        return getStatus(id, null);
    }

    /**
     * @param url  The downloading URL.
     * @param path The downloading file's path.
     * @return The downloading status.
     * @see #getStatus(int, String)
     * @see #getStatusIgnoreCompleted(int)
     */
    public byte getStatus(final String url, final String path) {
        return getStatus(FileDownloadUtils.generateId(url, path), path);
    }

    /**
     * @param id   The downloadId.
     * @param path The target file path.
     * @return the downloading status.
     * @see FileDownloadStatus
     * @see #getStatus(String, String)
     * @see #getStatusIgnoreCompleted(int)
     */
    public byte getStatus(final int id, final String path) {
        byte status;
        BaseDownloadTask.IRunningTask task = FileDownloadList.getImpl().get(id);
        if (task == null) {
            status = FileDownloadServiceProxy.getImpl().getStatus(id);
        } else {
            status = task.getOrigin().getStatus();
        }

        if (path != null && status == FileDownloadStatus.INVALID_STATUS) {
            if (FileDownloadUtils.isFilenameConverted(FileDownloadHelper.getAppContext())
                    && new File(path).exists()) {
                status = FileDownloadStatus.completed;
            }
        }

        return status;
    }

    /**
     * Find the running task by {@code url} and default path, and replace its listener with
     * the new one {@code listener}.
     *
     * @return The target task's DownloadId, if not exist target task, and replace failed, will be 0
     * @see #replaceListener(int, FileDownloadListener)
     * @see #replaceListener(String, String, FileDownloadListener)
     */
    public int replaceListener(String url, FileDownloadListener listener) {
        return replaceListener(url, FileDownloadUtils.getDefaultSaveFilePath(url), listener);
    }

    /**
     * Find the running task by {@code url} and {@code path}, and replace its listener with
     * the new one {@code listener}.
     *
     * @return The target task's DownloadId, if not exist target task, and replace failed, will be 0
     * @see #replaceListener(String, FileDownloadListener)
     * @see #replaceListener(int, FileDownloadListener)
     */
    public int replaceListener(String url, String path, FileDownloadListener listener) {
        return replaceListener(FileDownloadUtils.generateId(url, path), listener);
    }

    /**
     * Find the running task by {@code id}, and replace its listener width the new one
     * {@code listener}.
     *
     * @return The target task's DownloadId, if not exist target task, and replace failed, will be 0
     * @see #replaceListener(String, FileDownloadListener)
     * @see #replaceListener(String, String, FileDownloadListener)
     */
    public int replaceListener(int id, FileDownloadListener listener) {
        final BaseDownloadTask.IRunningTask task = FileDownloadList.getImpl().get(id);
        if (task == null) {
            return 0;
        }

        task.getOrigin().setListener(listener);
        return task.getOrigin().getId();
    }

    /**
     * Start and bind the FileDownloader service.
     *

     * Tips: The FileDownloader service will start and bind automatically when any
     * task is request to start.
     *
     * @see #bindService(Runnable)
     * @see #isServiceConnected()
     * @see #addServiceConnectListener(FileDownloadConnectListener)
     */
    public void bindService() {
        if (!isServiceConnected()) {
            FileDownloadServiceProxy.getImpl()
                    .bindStartByContext(FileDownloadHelper.getAppContext());
        }
    }

    /**
     * Start and bind the FileDownloader service and run {@code runnable} as soon as the binding is
     * successful.
     *

     * Tips: The FileDownloader service will start and bind automatically when any
     * task is request to start.
     *
     * @param runnable the command will be executed as soon as the FileDownloader Service is
     *                 successfully bound.
     * @see #isServiceConnected()
     * @see #bindService()
     * @see #addServiceConnectListener(FileDownloadConnectListener)
     */
    public void bindService(final Runnable runnable) {
        if (isServiceConnected()) {
            runnable.run();
        } else {
            FileDownloadServiceProxy.getImpl().
                    bindStartByContext(FileDownloadHelper.getAppContext(), runnable);
        }
    }

    /**
     * Unbind and stop the downloader service.
     */
    public void unBindService() {
        if (isServiceConnected()) {
            FileDownloadServiceProxy.getImpl().unbindByContext(FileDownloadHelper.getAppContext());
        }
    }

    /**
     * Unbind and stop the downloader service when there is no task running in the FileDownloader.
     *
     * @return {@code true} if unbind and stop the downloader service successfully, {@code false}
     * there are some tasks running in the FileDownloader.
     */
    public boolean unBindServiceIfIdle() {
        // check idle
        if (!isServiceConnected()) {
            return false;
        }

        if (FileDownloadList.getImpl().isEmpty()
                && FileDownloadServiceProxy.getImpl().isIdle()) {
            unBindService();
            return true;
        }

        return false;
    }

    /**
     * @return {@code true} if the downloader service has been started and connected.
     */
    public boolean isServiceConnected() {
        return FileDownloadServiceProxy.getImpl().isConnected();
    }

    /**
     * Add the listener for listening when the status of connection with the downloader service is
     * changed.
     *
     * @param listener The downloader service connection listener.
     * @see #removeServiceConnectListener(FileDownloadConnectListener)
     */
    public void addServiceConnectListener(final FileDownloadConnectListener listener) {
        FileDownloadEventPool.getImpl().addListener(DownloadServiceConnectChangedEvent.ID,
                listener);
    }

    /**
     * Remove the listener for listening when the status of connection with the downloader service
     * is changed.
     *
     * @param listener The downloader service connection listener.
     * @see #addServiceConnectListener(FileDownloadConnectListener)
     */
    public void removeServiceConnectListener(final FileDownloadConnectListener listener) {
        FileDownloadEventPool.getImpl().removeListener(DownloadServiceConnectChangedEvent.ID,
                listener);
    }

    /**
     * Start the {@code notification} with the {@code id}. This will let the downloader service
     * change to a foreground service.
     *

     * In foreground status, will save the FileDownloader alive, even user kill the application
     * from recent apps.
     *

     * Make FileDownloader service run in the foreground, supplying the ongoing
     * notification to be shown to the user while in this state.
     * By default FileDownloader services are background, meaning that if the system needs to
     * kill them to reclaim more memory (such as to display a large page in a
     * web browser), they can be killed without too much harm.  You can set this
     * flag if killing your service would be disruptive to the user, such as
     * if your service is performing background downloading, so the user
     * would notice if their app stopped downloading.
     *
     * @param id           The identifier for this notification as per
     *                     {@link android.app.NotificationManager#notify(int, Notification)
     *                     NotificationManager.notify(int, Notification)}; must not be 0.
     * @param notification The notification to be displayed.
     * @see #stopForeground(boolean)
     */
    public void startForeground(int id, Notification notification) {
        FileDownloadServiceProxy.getImpl().startForeground(id, notification);
    }

    /**
     * Remove the downloader service from the foreground state, allowing it to be killed if
     * more memory is needed.
     *
     * @param removeNotification {@code true} if the notification previously provided
     *                           to {@link #startForeground} will be removed. {@code false} it will
     *                           be remained until a later call removes it (or the service is
     *                           destroyed).
     * @see #startForeground(int, Notification)
     */
    public void stopForeground(boolean removeNotification) {
        FileDownloadServiceProxy.getImpl().stopForeground(removeNotification);
    }

    /**
     * @param url        The url of the completed task.
     * @param path       The absolute path of the completed task's save file.
     * @param totalBytes The content-length of the completed task, the length of the file in the
     *                   {@code path} must be equal to this value.
     * @return Whether is successful to set the task completed. If the {@code path} not exist will
     * be false; If the length of the file in {@code path} is not equal to {@code totalBytes} will
     * be false; If the task with {@code url} and {@code path} is downloading will be false.
     * Otherwise will be true.
     * @see FileDownloadUtils#isFilenameConverted(Context)
     *

     *

     * Recommend used to telling the FileDownloader Engine that the task with the {@code url}  and
     * the {@code path} has already completed downloading, in case of your task has already
     * downloaded by other ways(not by FileDownloader Engine), and after success to set the task
     * completed, FileDownloader will check the task with {@code url} and the {@code path} whether
     * completed by {@code totalBytes}.
     *

     * Otherwise, If FileDownloader Engine isn't know your task's status, whatever your task with
     * the {@code url} and the {@code path} has already downloaded in other way, FileDownloader
     * Engine will ignore the exist file and redownload it, because FileDownloader Engine don't know
     * the exist file whether it is valid.
     * @see #setTaskCompleted(List)
     * @deprecated If you invoked this method, please remove the code directly feel free, it doesn't
     * need any longer. In new mechanism(filedownloader 0.3.3 or higher), FileDownloader doesn't
     * store completed tasks in Database anymore, because all downloading files have temp a file
     * name.
     */
    @SuppressWarnings("UnusedParameters")
    public boolean setTaskCompleted(String url, String path, long totalBytes) {
        FileDownloadLog.w(this, "If you invoked this method, please remove it directly feel free, "
                + "it doesn't need any longer");
        return true;
    }

    /**
     * Recommend used to telling the FileDownloader Engine that a bulk of tasks have already
     * downloaded by other ways(not by the FileDownloader Engine).
     *

     * The FileDownloader Engine need to know the status of completed, because if you want to
     * download any tasks, FileDownloader Engine judges whether the task need downloads or not
     * according its status which existed in DB.
     *
     * @param taskAtomList The bulk of tasks.
     * @return Whether is successful to update all tasks' status to the Filedownloader Engine. If
     * one task atom among them is not match the Rules in
     * FileDownloadMgr#obtainCompletedTaskShelfModel(String, String, long)
     * will receive false, and non of them would be updated to DB.
     * @see #setTaskCompleted(String, String, long)
     * @deprecated If you invoked this method, please remove the code directly feel free, it doesn't
     * need any longer. In new mechanism(filedownloader 0.3.3 or higher), FileDownloader doesn't
     * store completed tasks in Database anymore, because all downloading files have temp a file
     * name.
     */
    @SuppressWarnings("UnusedParameters")
    public boolean setTaskCompleted(
            @SuppressWarnings("deprecation") List<FileDownloadTaskAtom> taskAtomList) {
        FileDownloadLog.w(this, "If you invoked this method, please remove it directly feel free, "
                + "it doesn't need any longer");
        return true;
    }

    /**
     * Set the maximum count of the network thread, what is the number of simultaneous downloads in
     * FileDownloader.
     *
     * @param count the number of simultaneous downloads, scope: [1, 12].
     * @return whether is successful to set the max network thread count.
     * If there are any actively executing tasks in FileDownloader, you will receive a warn
     * priority log int the logcat and this operation would be failed.
     */
    public boolean setMaxNetworkThreadCount(final int count) {
        if (!FileDownloadList.getImpl().isEmpty()) {
            FileDownloadLog.w(this, "Can't change the max network thread count, because there "
                    + "are actively executing tasks in FileDownloader, please try again after all"
                    + " actively executing tasks are completed or invoking FileDownloader#pauseAll"
                    + " directly.");
            return false;
        }

        return FileDownloadServiceProxy.getImpl().setMaxNetworkThreadCount(count);
    }

    /**
     * If the FileDownloader service is not started and connected, FileDownloader will try to start
     * it and try to bind with it. The current thread will also be blocked until the FileDownloader
     * service is started and a connection is established, and then the request you
     * invoke in {@link FileDownloadLine} will be executed.
     *

     * If the FileDownloader service has been started and connected, the request you invoke in
     * {@link FileDownloadLine} will be executed immediately.
     *

     * Note: FileDownloader can not block the main thread, because the system is
     * also call-backs the {@link ServiceConnection#onServiceConnected(ComponentName, IBinder)}
     * method in the main thread.
     *

     * Tips: The FileDownloader service will start and bind automatically when any
     * task is request to start.
     *
     * @see FileDownloadLine
     * @see #bindService(Runnable)
     */
    public FileDownloadLine insureServiceBind() {
        return new FileDownloadLine();
    }

    /**
     * If the FileDownloader service is not started and connected will return {@code false}
     * immediately, and meanwhile FileDownloader will try to start FileDownloader service and try to
     * bind with it, and after it is bound successfully the request you invoke in
     * {@link FileDownloadLineAsync} will be executed automatically.
     *

     * If the FileDownloader service has been started and connected, the request you invoke in
     * {@link FileDownloadLineAsync} will be executed immediately.
     *
     * @see FileDownloadLineAsync
     * @see #bindService(Runnable)
     */
    public FileDownloadLineAsync insureServiceBindAsync() {
        return new FileDownloadLineAsync();
    }

    private static final Object INIT_QUEUES_HANDLER_LOCK = new Object();
    private IQueuesHandler mQueuesHandler;

    IQueuesHandler getQueuesHandler() {
        if (mQueuesHandler == null) {
            synchronized (INIT_QUEUES_HANDLER_LOCK) {
                if (mQueuesHandler == null) {
                    mQueuesHandler = new QueuesHandler();
                }
            }
        }
        return mQueuesHandler;
    }

    private static final Object INIT_LOST_CONNECTED_HANDLER_LOCK = new Object();
    private ILostServiceConnectedHandler mLostConnectedHandler;

    ILostServiceConnectedHandler getLostConnectedHandler() {
        if (mLostConnectedHandler == null) {
            synchronized (INIT_LOST_CONNECTED_HANDLER_LOCK) {
                if (mLostConnectedHandler == null) {
                    mLostConnectedHandler = new LostServiceConnectedHandler();
                    addServiceConnectListener((FileDownloadConnectListener) mLostConnectedHandler);
                }
            }
        }

        return mLostConnectedHandler;
    }

}