/*
 * 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.text.TextUtils;

import com.liulishuo.filedownloader.IThreadPoolMonitor;
import com.liulishuo.filedownloader.PauseAllMarker;
import com.liulishuo.filedownloader.database.FileDownloadDatabase;
import com.liulishuo.filedownloader.download.CustomComponentHolder;
import com.liulishuo.filedownloader.download.DownloadLaunchRunnable;
import com.liulishuo.filedownloader.download.DownloadRunnable;
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.util.FileDownloadHelper;
import com.liulishuo.filedownloader.util.FileDownloadLog;
import com.liulishuo.filedownloader.util.FileDownloadUtils;

import java.util.List;

/**
 * The downloading manager in FileDownloadService, which is used to control all download-inflow.
 * <p/>
 * Handling real {@link #start(String, String, boolean, int, int, int, boolean, FileDownloadHeader,
 * boolean)}.
 *
 * @see FileDownloadThreadPool
 * @see DownloadLaunchRunnable
 * @see DownloadRunnable
 */
class FileDownloadManager implements IThreadPoolMonitor {
    private final FileDownloadDatabase mDatabase;
    private final FileDownloadThreadPool mThreadPool;

    FileDownloadManager() {
        final CustomComponentHolder holder = CustomComponentHolder.getImpl();
        this.mDatabase = holder.getDatabaseInstance();
        this.mThreadPool = new FileDownloadThreadPool(holder.getMaxNetworkThreadCount());
    }

    // synchronize for safe: check downloading, check resume, update data, execute runnable
    public synchronized void start(final String url, final String path,
                                   final boolean pathAsDirectory,
                                   final int callbackProgressTimes,
                                   final int callbackProgressMinIntervalMillis,
                                   final int autoRetryTimes, final boolean forceReDownload,
                                   final FileDownloadHeader header, final boolean isWifiRequired) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "request start the task with url(%s) path(%s) isDirectory(%B)",
                    url, path, pathAsDirectory);
        }

        // clear pause all marker so that the delay pause all check doesn't obstruct normal start of
        // new task
        PauseAllMarker.clearMarker();

        final int id = FileDownloadUtils.generateId(url, path, pathAsDirectory);
        FileDownloadModel model = mDatabase.find(id);

        List<ConnectionModel> dirConnectionModelList = null;

        if (!pathAsDirectory && model == null) {
            // try dir data.
            final int dirCaseId = FileDownloadUtils
                    .generateId(url, FileDownloadUtils.getParent(path),
                            true);
            model = mDatabase.find(dirCaseId);
            if (model != null && path.equals(model.getTargetFilePath())) {
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(this, "task[%d] find model by dirCaseId[%d]", id, dirCaseId);
                }

                dirConnectionModelList = mDatabase.findConnectionModel(dirCaseId);
            }
        }

        if (FileDownloadHelper.inspectAndInflowDownloading(id, model, this, true)) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "has already started download %d", id);
            }
            return;
        }

        final String targetFilePath = model != null ? model.getTargetFilePath()
                : FileDownloadUtils.getTargetFilePath(path, pathAsDirectory, null);
        if (FileDownloadHelper.inspectAndInflowDownloaded(id, targetFilePath, forceReDownload,
                true)) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "has already completed downloading %d", id);
            }
            return;
        }

        final long sofar = model != null ? model.getSoFar() : 0;
        final String tempFilePath = model != null ? model.getTempFilePath()
                : FileDownloadUtils.getTempPath(targetFilePath);
        if (FileDownloadHelper.inspectAndInflowConflictPath(id, sofar, tempFilePath, targetFilePath,
                this)) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog
                        .d(this, "there is an another task with the same target-file-path %d %s",
                                id, targetFilePath);
            }

            // because of the file is dirty for this task.
            if (model != null) {
                mDatabase.remove(id);
                mDatabase.removeConnections(id);
            }
            return;
        }

        // real start
        // - create model
        boolean needUpdate2DB;
        if (model != null
                && (model.getStatus() == FileDownloadStatus.paused
                || model.getStatus() == FileDownloadStatus.error
                || model.getStatus() == FileDownloadStatus.pending
                || model.getStatus() == FileDownloadStatus.started
                || model.getStatus() == FileDownloadStatus.connected) // FileDownloadRunnable
            // invoke #isBreakpointAvailable to determine whether it is really invalid.
                ) {
            if (model.getId() != id) {
                // in try dir case.
                mDatabase.remove(model.getId());
                mDatabase.removeConnections(model.getId());

                model.setId(id);
                model.setPath(path, pathAsDirectory);
                if (dirConnectionModelList != null) {
                    for (ConnectionModel connectionModel : dirConnectionModelList) {
                        connectionModel.setId(id);
                        mDatabase.insertConnectionModel(connectionModel);
                    }
                }

                needUpdate2DB = true;
            } else {
                if (!TextUtils.equals(url, model.getUrl())) {
                    // for cover the case of reusing the downloaded processing with the different
                    // url( using with idGenerator ).
                    model.setUrl(url);
                    needUpdate2DB = true;
                } else {
                    needUpdate2DB = false;
                }
            }
        } else {
            if (model == null) {
                model = new FileDownloadModel();
            }
            model.setUrl(url);
            model.setPath(path, pathAsDirectory);

            model.setId(id);
            model.setSoFar(0);
            model.setTotal(0);
            model.setStatus(FileDownloadStatus.pending);
            model.setConnectionCount(1);
            needUpdate2DB = true;
        }

        // - update model to db
        if (needUpdate2DB) {
            mDatabase.update(model);
        }

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

        final DownloadLaunchRunnable runnable =
                builder.setModel(model)
                        .setHeader(header)
                        .setThreadPoolMonitor(this)
                        .setMinIntervalMillis(callbackProgressMinIntervalMillis)
                        .setCallbackProgressMaxCount(callbackProgressTimes)
                        .setForceReDownload(forceReDownload)
                        .setWifiRequired(isWifiRequired)
                        .setMaxRetryTimes(autoRetryTimes)
                        .build();

        // - execute
        mThreadPool.execute(runnable);

    }

    public boolean isDownloading(String url, String path) {
        return isDownloading(FileDownloadUtils.generateId(url, path));
    }

    public boolean isDownloading(int id) {
        return isDownloading(mDatabase.find(id));
    }

    public boolean pause(final int id) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "request pause the task %d", id);
        }

        final FileDownloadModel model = mDatabase.find(id);
        if (model == null) {
            return false;
        }

        model.setStatus(FileDownloadStatus.paused);
        mThreadPool.cancel(id);
        return true;
    }

    /**
     * Pause all running task
     */
    public void pauseAll() {
        List<Integer> list = mThreadPool.getAllExactRunningDownloadIds();

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "pause all tasks %d", list.size());
        }

        for (Integer id : list) {
            pause(id);
        }
    }

    public long getSoFar(final int id) {
        final FileDownloadModel model = mDatabase.find(id);
        if (model == null) {
            return 0;
        }

        final int connectionCount = model.getConnectionCount();
        if (connectionCount <= 1) {
            return model.getSoFar();
        } else {
            final List<ConnectionModel> modelList = mDatabase.findConnectionModel(id);
            if (modelList == null || modelList.size() != connectionCount) {
                return 0;
            } else {
                return ConnectionModel.getTotalOffset(modelList);
            }
        }
    }

    public long getTotal(final int id) {
        final FileDownloadModel model = mDatabase.find(id);
        if (model == null) {
            return 0;
        }

        return model.getTotal();
    }

    public byte getStatus(final int id) {
        final FileDownloadModel model = mDatabase.find(id);
        if (model == null) {
            return FileDownloadStatus.INVALID_STATUS;
        }

        return model.getStatus();
    }

    public boolean isIdle() {
        return mThreadPool.exactSize() <= 0;
    }

    public synchronized boolean setMaxNetworkThreadCount(int count) {
        return mThreadPool.setMaxNetworkThreadCount(count);
    }

    @Override
    public boolean isDownloading(FileDownloadModel model) {
        if (model == null) {
            return false;
        }

        final boolean isInPool = mThreadPool.isInThreadPool(model.getId());
        boolean isDownloading;

        do {
            if (FileDownloadStatus.isOver(model.getStatus())) {

                //noinspection RedundantIfStatement
                if (isInPool) {
                    // already finished, but still in the pool.
                    // handle as downloading.
                    isDownloading = true;
                } else {
                    // already finished, and not in the pool.
                    // make sense.
                    isDownloading = false;

                }
            } else {
                if (isInPool) {
                    // not finish, in the pool.
                    // make sense.
                    isDownloading = true;
                } else {
                    // not finish, but not in the pool.
                    // beyond expectation.
                    FileDownloadLog.e(this, "%d status is[%s](not finish) & but not in the pool",
                            model.getId(), model.getStatus());
                    // handle as not in downloading, going to re-downloading.
                    isDownloading = false;

                }
            }
        } while (false);

        return isDownloading;
    }

    @Override
    public int findRunningTaskIdBySameTempPath(String tempFilePath, int excludeId) {
        return mThreadPool.findRunningTaskIdBySameTempPath(tempFilePath, excludeId);
    }

    public boolean clearTaskData(int id) {
        if (id == 0) {
            FileDownloadLog.w(this, "The task[%d] id is invalid, can't clear it.", id);
            return false;
        }

        if (isDownloading(id)) {
            FileDownloadLog.w(this, "The task[%d] is downloading, can't clear it.", id);
            return false;
        }

        mDatabase.remove(id);
        mDatabase.removeConnections(id);
        return true;
    }

    public void clearAllTaskData() {
        mDatabase.clear();
    }
}


/*
 * 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 com.liulishuo.filedownloader.message.BlockCompleteMessage;
import com.liulishuo.filedownloader.message.MessageSnapshot;
import com.liulishuo.filedownloader.model.FileDownloadStatus;
import com.liulishuo.filedownloader.util.FileDownloadLog;
import com.liulishuo.filedownloader.util.FileDownloadUtils;

import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * The messenger for sending messages to  {@link FileDownloadListener}.
 *
 * @see IFileDownloadMessenger
 */
class FileDownloadMessenger implements IFileDownloadMessenger {

    private BaseDownloadTask.IRunningTask mTask;
    private BaseDownloadTask.LifeCycleCallback mLifeCycleCallback;

    private Queue<MessageSnapshot> parcelQueue;

    private boolean mIsDiscard = false;

    FileDownloadMessenger(final BaseDownloadTask.IRunningTask task,
                          final BaseDownloadTask.LifeCycleCallback callback) {
        init(task, callback);
    }

    private void init(final BaseDownloadTask.IRunningTask task,
                      BaseDownloadTask.LifeCycleCallback callback) {
        this.mTask = task;
        this.mLifeCycleCallback = callback;
        parcelQueue = new LinkedBlockingQueue<>();
    }

    @Override
    public boolean notifyBegin() {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify begin %s", mTask);
        }

        if (mTask == null) {
            FileDownloadLog.w(this, "can't begin the task, the holder fo the messenger is nil, %d",
                    parcelQueue.size());
            return false;
        }

        mLifeCycleCallback.onBegin();

        return true;
    }

    @Override
    public void notifyPending(MessageSnapshot snapshot) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify pending %s", mTask);
        }

        mLifeCycleCallback.onIng();

        process(snapshot);
    }

    @Override
    public void notifyStarted(MessageSnapshot snapshot) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify started %s", mTask);
        }

        mLifeCycleCallback.onIng();

        process(snapshot);
    }

    @Override
    public void notifyConnected(MessageSnapshot snapshot) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify connected %s", mTask);
        }

        mLifeCycleCallback.onIng();

        process(snapshot);
    }

    @Override
    public void notifyProgress(MessageSnapshot snapshot) {
        final BaseDownloadTask originTask = mTask.getOrigin();
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify progress %s %d %d",
                    originTask, originTask.getLargeFileSoFarBytes(),
                    originTask.getLargeFileTotalBytes());
        }
        if (originTask.getCallbackProgressTimes() <= 0) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "notify progress but client not request notify %s", mTask);
            }
            return;
        }

        mLifeCycleCallback.onIng();

        process(snapshot);

    }

    /**
     * sync
     */
    @Override
    public void notifyBlockComplete(MessageSnapshot snapshot) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify block completed %s %s", mTask,
                    Thread.currentThread().getName());
        }

        mLifeCycleCallback.onIng();

        process(snapshot);
    }

    @Override
    public void notifyRetry(MessageSnapshot snapshot) {
        if (FileDownloadLog.NEED_LOG) {
            final BaseDownloadTask originTask = mTask.getOrigin();
            FileDownloadLog.d(this, "notify retry %s %d %d %s", mTask,
                    originTask.getAutoRetryTimes(), originTask.getRetryingTimes(),
                    originTask.getErrorCause());
        }

        mLifeCycleCallback.onIng();

        process(snapshot);
    }

    // Over state, from FileDownloadList, to user -----------------------------
    @Override
    public void notifyWarn(MessageSnapshot snapshot) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify warn %s", mTask);
        }

        mLifeCycleCallback.onOver();

        process(snapshot);
    }

    @Override
    public void notifyError(MessageSnapshot snapshot) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify error %s %s", mTask, mTask.getOrigin().getErrorCause());
        }

        mLifeCycleCallback.onOver();

        process(snapshot);
    }

    @Override
    public void notifyPaused(MessageSnapshot snapshot) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify paused %s", mTask);
        }

        mLifeCycleCallback.onOver();

        process(snapshot);
    }

    @Override
    public void notifyCompleted(MessageSnapshot snapshot) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "notify completed %s", mTask);
        }

        mLifeCycleCallback.onOver();

        process(snapshot);
    }

    private void process(MessageSnapshot snapshot) {
        if (mTask == null) {
            if (FileDownloadLog.NEED_LOG) {
                // the most possible of occurring this case is the thread for flowing the paused
                // message is different with others.
                FileDownloadLog.d(this, "occur this case, it would be the host task of this "
                                + "messenger has been over(paused/warn/completed/error) on the "
                                + "other thread before receiving the snapshot(id[%d], status[%d])",
                        snapshot.getId(), snapshot.getStatus());
            }
            return;
        }

        if (mIsDiscard || mTask.getOrigin().getListener() == null) {
            if ((FileDownloadMonitor.isValid() || mTask.isContainFinishListener())
                    && snapshot.getStatus() == FileDownloadStatus.blockComplete) {
                // there is a FileDownloadMonitor, so we have to ensure the 'BaseDownloadTask#over'
                // can be invoked.
                mLifeCycleCallback.onOver();
            }

            inspectAndHandleOverStatus(snapshot.getStatus());
        } else {
            parcelQueue.offer(snapshot);

            FileDownloadMessageStation.getImpl().requestEnqueue(this);
        }
    }

    private void inspectAndHandleOverStatus(int status) {
        // If this task is in the over state, try to retire this messenger.
        if (FileDownloadStatus.isOver(status)) {
            if (!parcelQueue.isEmpty()) {
                final MessageSnapshot queueTopTask = parcelQueue.peek();
                FileDownloadLog.w(this,
                        "the messenger[%s](with id[%d]) has already "
                                + "accomplished all his job, but there still are some messages in"
                                + " parcel queue[%d] queue-top-status[%d]",
                        this, queueTopTask.getId(), parcelQueue.size(), queueTopTask.getStatus());
            }
            mTask = null;
        }
    }

    @Override
    public void handoverMessage() {
        if (mIsDiscard) {
            return;
        }

        final MessageSnapshot message = parcelQueue.poll();
        final int currentStatus = message.getStatus();
        final BaseDownloadTask.IRunningTask task = mTask;

        if (task == null) {
            throw new IllegalArgumentException(FileDownloadUtils.formatString(
                    "can't handover the message, no master to receive this "
                            + "message(status[%d]) size[%d]",
                    currentStatus, parcelQueue.size()));
        }

        final BaseDownloadTask originTask = task.getOrigin();

        final FileDownloadListener listener = originTask.getListener();
        final ITaskHunter.IMessageHandler messageHandler = task.getMessageHandler();

        inspectAndHandleOverStatus(currentStatus);

        if (listener == null || listener.isInvalid()) {
            return;
        }

        if (currentStatus == FileDownloadStatus.blockComplete) {
            try {
                listener.blockComplete(originTask);
                notifyCompleted(((BlockCompleteMessage) message).transmitToCompleted());
            } catch (Throwable throwable) {
                notifyError(messageHandler.prepareErrorMessage(throwable));
            }
        } else {
            FileDownloadLargeFileListener largeFileListener = null;
            if (listener instanceof FileDownloadLargeFileListener) {
                largeFileListener = (FileDownloadLargeFileListener) listener;
            }

            switch (currentStatus) {
                case FileDownloadStatus.pending:
                    if (largeFileListener != null) {
                        largeFileListener.pending(originTask,
                                message.getLargeSofarBytes(),
                                message.getLargeTotalBytes());
                    } else {
                        listener.pending(originTask,
                                message.getSmallSofarBytes(),
                                message.getSmallTotalBytes());
                    }

                    break;
                case FileDownloadStatus.started:
                    listener.started(originTask);
                    break;
                case FileDownloadStatus.connected:
                    if (largeFileListener != null) {
                        largeFileListener.connected(originTask,
                                message.getEtag(),
                                message.isResuming(),
                                originTask.getLargeFileSoFarBytes(),
                                message.getLargeTotalBytes());

                    } else {
                        listener.connected(originTask,
                                message.getEtag(),
                                message.isResuming(),
                                originTask.getSmallFileSoFarBytes(),
                                message.getSmallTotalBytes());
                    }

                    break;
                case FileDownloadStatus.progress:
                    if (largeFileListener != null) {
                        largeFileListener.progress(originTask,
                                message.getLargeSofarBytes(),
                                originTask.getLargeFileTotalBytes());

                    } else {
                        listener.progress(originTask,
                                message.getSmallSofarBytes(),
                                originTask.getSmallFileTotalBytes());
                    }
                    break;
                case FileDownloadStatus.retry:
                    if (largeFileListener != null) {
                        largeFileListener.retry(originTask,
                                message.getThrowable(),
                                message.getRetryingTimes(),
                                message.getLargeSofarBytes());
                    } else {
                        listener.retry(originTask,
                                message.getThrowable(),
                                message.getRetryingTimes(),
                                message.getSmallSofarBytes());
                    }

                    break;
                case FileDownloadStatus.completed:
                    listener.completed(originTask);
                    break;
                case FileDownloadStatus.error:
                    listener.error(originTask,
                            message.getThrowable());
                    break;
                case FileDownloadStatus.paused:
                    if (largeFileListener != null) {
                        largeFileListener.paused(originTask,
                                message.getLargeSofarBytes(),
                                message.getLargeTotalBytes());
                    } else {
                        listener.paused(originTask,
                                message.getSmallSofarBytes(),
                                message.getSmallTotalBytes());
                    }
                    break;
                case FileDownloadStatus.warn:
                    // already same url & path in pending/running list
                    listener.warn(originTask);
                    break;
                default:
                    // ignored
            }
        }
    }

    @Override
    public boolean handoverDirectly() {
        return mTask.getOrigin().isSyncCallback();
    }

    @Override
    public void reAppointment(BaseDownloadTask.IRunningTask task,
                              BaseDownloadTask.LifeCycleCallback callback) {
        if (this.mTask != null) {
            throw new IllegalStateException(
                    FileDownloadUtils.formatString("the messenger is working, can't "
                            + "re-appointment for %s", task));
        }

        init(task, callback);
    }

    @Override
    public boolean isBlockingCompleted() {
        return parcelQueue.peek().getStatus() == FileDownloadStatus.blockComplete;
    }

    @Override
    public void discard() {
        mIsDiscard = true;
    }

    @Override
    public String toString() {
        return FileDownloadUtils.formatString("%d:%s",
                mTask == null ? -1 : mTask.getOrigin().getId(), super.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 java.util.ArrayList;
import java.util.List;

/**
 * The helper for start and config the task queue simply and quickly.
 *
 * @see FileDownloader#start(FileDownloadListener, boolean)
 */
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public class FileDownloadQueueSet {

    private FileDownloadListener target;
    private boolean isSerial;

    private List<BaseDownloadTask.FinishListener> taskFinishListenerList;
    private Integer autoRetryTimes;
    private Boolean syncCallback;
    private Boolean isForceReDownload;
    private Boolean isWifiRequired;
    private Integer callbackProgressTimes;
    private Integer callbackProgressMinIntervalMillis;
    private Object tag;
    private String directory;

    private BaseDownloadTask[] tasks;

    /**
     * @param target The download listener will be set to all tasks in this queue set.
     */
    public FileDownloadQueueSet(FileDownloadListener target) {
        if (target == null) {
            throw new IllegalArgumentException(
                    "create FileDownloadQueueSet must with valid target!");
        }
        this.target = target;
    }

    /**
     * Form a queue with same {@link #target} and will {@link #start()} in parallel.
     */
    public FileDownloadQueueSet downloadTogether(BaseDownloadTask... tasks) {
        this.isSerial = false;
        this.tasks = tasks;

        return this;

    }

    /**
     * Form a queue with same {@link #target} and will {@link #start()} in parallel.
     */
    public FileDownloadQueueSet downloadTogether(List<BaseDownloadTask> tasks) {
        this.isSerial = false;
        this.tasks = new BaseDownloadTask[tasks.size()];
        tasks.toArray(this.tasks);

        return this;

    }

    /**
     * Form a queue with same {@link #target} and will {@link #start()} linearly.
     */
    public FileDownloadQueueSet downloadSequentially(BaseDownloadTask... tasks) {
        this.isSerial = true;
        this.tasks = tasks;

        return this;
    }

    /**
     * Form a queue with same {@link #target} and will {@link #start()} linearly.
     */
    public FileDownloadQueueSet downloadSequentially(List<BaseDownloadTask> tasks) {
        this.isSerial = true;
        this.tasks = new BaseDownloadTask[tasks.size()];
        tasks.toArray(this.tasks);

        return this;
    }

    /**
     * Before starting downloading tasks in this queue-set, we will try to
     * {@link BaseDownloadTask#reuse} tasks first.
     */
    public void reuseAndStart() {
        for (BaseDownloadTask task : tasks) {
            task.reuse();
        }
        start();
    }

    /**
     * Start tasks in a queue.
     *
     * @see #downloadSequentially(BaseDownloadTask...)
     * @see #downloadSequentially(List)
     * @see #downloadTogether(BaseDownloadTask...)
     * @see #downloadTogether(List)
     */
    public void start() {
        for (BaseDownloadTask task : tasks) {
            task.setListener(target);

            if (autoRetryTimes != null) {
                task.setAutoRetryTimes(autoRetryTimes);
            }

            if (syncCallback != null) {
                task.setSyncCallback(syncCallback);
            }

            if (isForceReDownload != null) {
                task.setForceReDownload(isForceReDownload);
            }

            if (callbackProgressTimes != null) {
                task.setCallbackProgressTimes(callbackProgressTimes);
            }

            if (callbackProgressMinIntervalMillis != null) {
                task.setCallbackProgressMinInterval(callbackProgressMinIntervalMillis);
            }

            if (tag != null) {
                task.setTag(tag);
            }

            if (taskFinishListenerList != null) {
                for (BaseDownloadTask.FinishListener finishListener : taskFinishListenerList) {
                    task.addFinishListener(finishListener);
                }
            }

            if (this.directory != null) {
                task.setPath(this.directory, true);
            }

            if (this.isWifiRequired != null) {
                task.setWifiRequired(this.isWifiRequired);
            }

            task.asInQueueTask().enqueue();
        }

        FileDownloader.getImpl().start(target, isSerial);
    }

    /**
     * @param directory Set the {@code directory} to store files in this queue.
     *                  All tasks in this queue will be invoked
     *                  {@link BaseDownloadTask#setPath(String, boolean)} with params:
     *                  ({@code directory}, {@code true}).
     */
    public FileDownloadQueueSet setDirectory(String directory) {
        this.directory = directory;
        return this;
    }

    /**
     * @see BaseDownloadTask#setAutoRetryTimes(int)
     */
    public FileDownloadQueueSet setAutoRetryTimes(int autoRetryTimes) {
        this.autoRetryTimes = autoRetryTimes;
        return this;
    }

    /**
     * @see BaseDownloadTask#setSyncCallback(boolean)
     */
    public FileDownloadQueueSet setSyncCallback(final boolean syncCallback) {
        this.syncCallback = syncCallback;
        return this;
    }

    /**
     * @see BaseDownloadTask#setForceReDownload(boolean)
     */
    public FileDownloadQueueSet setForceReDownload(final boolean isForceReDownload) {
        this.isForceReDownload = isForceReDownload;
        return this;
    }

    /**
     * @see BaseDownloadTask#setCallbackProgressTimes(int)
     */
    public FileDownloadQueueSet setCallbackProgressTimes(final int callbackProgressTimes) {
        this.callbackProgressTimes = callbackProgressTimes;
        return this;
    }

    /**
     * @see BaseDownloadTask#setCallbackProgressMinInterval(int)
     */
    public FileDownloadQueueSet setCallbackProgressMinInterval(int minIntervalMillis) {
        this.callbackProgressMinIntervalMillis = minIntervalMillis;
        return this;
    }

    /**
     * @see BaseDownloadTask#setCallbackProgressIgnored()
     */
    public FileDownloadQueueSet ignoreEachTaskInternalProgress() {
        setCallbackProgressTimes(-1);
        return this;
    }

    /**
     * @see BaseDownloadTask#setCallbackProgressTimes(int)
     */
    public FileDownloadQueueSet disableCallbackProgressTimes() {
        return setCallbackProgressTimes(0);
    }

    /**
     * @see BaseDownloadTask#setTag(Object)
     */
    public FileDownloadQueueSet setTag(final Object tag) {
        this.tag = tag;
        return this;
    }

    /**
     * @see BaseDownloadTask#addFinishListener(BaseDownloadTask.FinishListener)
     */
    public FileDownloadQueueSet addTaskFinishListener(
            final BaseDownloadTask.FinishListener finishListener) {
        if (this.taskFinishListenerList == null) {
            this.taskFinishListenerList = new ArrayList<>();
        }

        this.taskFinishListenerList.add(finishListener);
        return this;
    }

    /**
     * @see BaseDownloadTask#setWifiRequired(boolean)
     */
    public FileDownloadQueueSet setWifiRequired(boolean isWifiRequired) {
        this.isWifiRequired = isWifiRequired;
        return this;
    }

}


/*
 * 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 com.liulishuo.filedownloader.message.MessageSnapshot;
import com.liulishuo.filedownloader.message.MessageSnapshotTaker;
import com.liulishuo.filedownloader.model.FileDownloadStatus;
import com.liulishuo.filedownloader.util.FileDownloadLog;

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

/**
 * Storing all tasks in processing in the Main-Process.
 */
@SuppressWarnings("UnusedReturnValue")
public class FileDownloadList {

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

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

    private final ArrayList<BaseDownloadTask.IRunningTask> mList;

    private FileDownloadList() {
        mList = new ArrayList<>();
    }

    boolean isEmpty() {
        return mList.isEmpty();
    }

    int size() {
        return mList.size();
    }

    /**
     * @param id download id
     * @return get counts os same id
     */
    int count(final int id) {
        int size = 0;
        synchronized (mList) {
            for (BaseDownloadTask.IRunningTask task : mList) {
                if (task.is(id)) {
                    size++;
                }
            }
        }
        return size;
    }

    public BaseDownloadTask.IRunningTask get(final int id) {
        synchronized (mList) {
            for (BaseDownloadTask.IRunningTask task : mList) {
                // when FileDownloadMgr#isDownloading
                if (task.is(id)) {
                    return task;
                }
            }
        }
        return null;
    }

    List<BaseDownloadTask.IRunningTask> getReceiveServiceTaskList(final int id) {
        final List<BaseDownloadTask.IRunningTask> list = new ArrayList<>();
        synchronized (this.mList) {
            for (BaseDownloadTask.IRunningTask task : this.mList) {
                if (task.is(id) && !task.isOver()) {

                    final byte status = task.getOrigin().getStatus();
                    if (status != FileDownloadStatus.INVALID_STATUS
                            && status != FileDownloadStatus.toLaunchPool) {
                        list.add(task);
                    }
                }
            }
        }

        return list;
    }

    List<BaseDownloadTask.IRunningTask> getDownloadingList(final int id) {
        final List<BaseDownloadTask.IRunningTask> list = new ArrayList<>();
        synchronized (this.mList) {
            for (BaseDownloadTask.IRunningTask task : this.mList) {
                if (task.is(id)
                        && !task.isOver()) {
                    list.add(task);
                }
            }
        }

        return list;
    }

    boolean isNotContains(final BaseDownloadTask.IRunningTask download) {
        return mList.isEmpty() || !mList.contains(download);
    }

    List<BaseDownloadTask.IRunningTask> copy(final FileDownloadListener listener) {
        final List<BaseDownloadTask.IRunningTask> targetList = new ArrayList<>();
        synchronized (mList) {
            // Prevent size changing
            for (BaseDownloadTask.IRunningTask task : mList) {
                if (task.is(listener)) {
                    targetList.add(task);
                }
            }
            return targetList;
        }
    }

    List<BaseDownloadTask.IRunningTask> assembleTasksToStart(int attachKey,
                                                             FileDownloadListener listener) {
        final List<BaseDownloadTask.IRunningTask> targetList = new ArrayList<>();
        synchronized (mList) {
            // Prevent size changing
            for (BaseDownloadTask.IRunningTask task : mList) {
                if (task.getOrigin().getListener() == listener && !task.getOrigin().isAttached()) {
                    task.setAttachKeyByQueue(attachKey);
                    targetList.add(task);
                }
            }
            return targetList;
        }
    }

    BaseDownloadTask.IRunningTask[] copy() {
        synchronized (mList) {
            // Prevent size changing
            BaseDownloadTask.IRunningTask[] copy = new BaseDownloadTask.IRunningTask[mList.size()];
            return mList.toArray(copy);
        }
    }

    /**
     * Divert all data in list 2 destination list
     */
    void divertAndIgnoreDuplicate(
            @SuppressWarnings("SameParameterValue") final List<BaseDownloadTask.IRunningTask>
                    destination) {
        synchronized (mList) {
            for (BaseDownloadTask.IRunningTask iRunningTask : mList) {
                if (!destination.contains(iRunningTask)) {
                    destination.add(iRunningTask);
                }
            }
            mList.clear();
        }
    }

    /**
     * @param willRemoveDownload will be remove
     */
    public boolean remove(final BaseDownloadTask.IRunningTask willRemoveDownload,
                          MessageSnapshot snapshot) {
        final byte removeByStatus = snapshot.getStatus();
        boolean succeed;
        synchronized (mList) {
            succeed = mList.remove(willRemoveDownload);
            if (succeed && mList.size() == 0) {
                if (FileDownloadServiceProxy.getImpl().isRunServiceForeground()) {
                    FileDownloader.getImpl().stopForeground(true);
                }
            }
        }
        if (FileDownloadLog.NEED_LOG) {
            if (mList.size() == 0) {
                FileDownloadLog.v(this, "remove %s left %d %d",
                        willRemoveDownload, removeByStatus, mList.size());
            }
        }

        if (succeed) {
            final IFileDownloadMessenger messenger = willRemoveDownload.getMessageHandler().
                    getMessenger();
            // Notify 2 Listener
            switch (removeByStatus) {
                case FileDownloadStatus.warn:
                    messenger.notifyWarn(snapshot);
                    break;
                case FileDownloadStatus.error:
                    messenger.notifyError(snapshot);
                    break;
                case FileDownloadStatus.paused:
                    messenger.notifyPaused(snapshot);
                    break;
                case FileDownloadStatus.completed:
                    messenger
                            .notifyBlockComplete(MessageSnapshotTaker.takeBlockCompleted(snapshot));
                    break;
                default:
                    // ignored
            }
        } else {
            FileDownloadLog.e(this, "remove error, not exist: %s %d", willRemoveDownload,
                    removeByStatus);
        }

        return succeed;
    }

    void add(final BaseDownloadTask.IRunningTask task) {
        if (!task.getOrigin().isAttached()) {
            // if this task didn't attach to any key, this task must be an isolated task, so we
            // generate a key and attache it to this task, make sure this task not be assembled by
            // a queue.
            task.setAttachKeyDefault();
        }

        if (task.getMessageHandler().getMessenger().notifyBegin()) {
            addUnchecked(task);
        }
    }

    /**
     * This method generally used for enqueuing the task which will be assembled by a queue.
     *
     * @see BaseDownloadTask.InQueueTask#enqueue()
     */
    void addUnchecked(final BaseDownloadTask.IRunningTask task) {
        if (task.isMarkedAdded2List()) {
            return;
        }

        synchronized (mList) {
            if (mList.contains(task)) {
                FileDownloadLog.w(this, "already has %s", task);
            } else {
                task.markAdded2List();
                mList.add(task);
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.v(this, "add list in all %s %d %d", task,
                            task.getOrigin().getStatus(), mList.size());
                }
            }
        }
    }
}


/*
 * 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.util;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;

import com.liulishuo.filedownloader.BaseDownloadTask;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * The serial queue, what used to dynamically increase tasks, and tasks in the queue will
 * automatically start download one by one.
 */

public class FileDownloadSerialQueue {

    private final Object operationLock = new Object();
    private final BlockingQueue<BaseDownloadTask> mTasks = new LinkedBlockingQueue<>();
    private final List<BaseDownloadTask> pausedList = new ArrayList<>();
    private final HandlerThread mHandlerThread;
    private final Handler mHandler;

    private static final int WHAT_NEXT = 1;
    public static final int ID_INVALID = 0;

    volatile BaseDownloadTask workingTask;
    final SerialFinishCallback finishCallback;
    volatile boolean paused = false;

    public FileDownloadSerialQueue() {
        mHandlerThread = new HandlerThread(
                FileDownloadUtils.getThreadPoolName("SerialDownloadManager"));
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper(), new SerialLoop());
        finishCallback = new SerialFinishCallback(new WeakReference<>(this));
        sendNext();
    }

    /**
     * Enqueues the given task sometime in the serial queue. If the {@code task} is in the head of
     * the serial queue, the {@code task} will be started automatically.
     */
    public void enqueue(BaseDownloadTask task) {
        synchronized (finishCallback) {
            if (paused) {
                pausedList.add(task);
                return;
            }

            try {
                mTasks.put(task);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Pause the queue.
     *
     * @see #resume()
     */
    public void pause() {
        synchronized (finishCallback) {
            if (paused) {
                FileDownloadLog.w(this, "require pause this queue(remain %d), but "
                        + "it has already been paused", mTasks.size());
                return;
            }

            paused = true;
            mTasks.drainTo(pausedList);
            if (workingTask != null) {
                workingTask.removeFinishListener(finishCallback);
                workingTask.pause();
            }
        }

    }

    /**
     * Resume the queue if the queue is paused.
     *
     * @see #pause()
     */
    public void resume() {
        synchronized (finishCallback) {
            if (!paused) {
                FileDownloadLog.w(this, "require resume this queue(remain %d), but it is"
                        + " still running", mTasks.size());
                return;
            }

            paused = false;
            mTasks.addAll(pausedList);
            pausedList.clear();

            if (workingTask == null) {
                sendNext();
            } else {
                workingTask.addFinishListener(finishCallback);
                workingTask.start();
            }
        }
    }

    /**
     * Returns the identify of the working task, if there is task is working, you will receive
     * {@link #ID_INVALID}.
     *
     * @return the identify of the working task
     */
    public int getWorkingTaskId() {
        return workingTask != null ? workingTask.getId() : ID_INVALID;
    }

    /**
     * Get the count of tasks which is waiting on this queue.
     *
     * @return the count of waiting tasks on this queue.
     */
    public int getWaitingTaskCount() {
        return mTasks.size() + pausedList.size();
    }

    /**
     * Attempts to stop the working task, halts the processing of waiting tasks, and returns a list
     * of the tasks that were awaiting execution. These tasks are drained (removed) from the task
     * queue upon return from this method.
     */
    public List<BaseDownloadTask> shutdown() {
        synchronized (finishCallback) {
            if (workingTask != null) {
                pause();
            }

            final List<BaseDownloadTask> unDealTaskList = new ArrayList<>(pausedList);
            pausedList.clear();
            mHandler.removeMessages(WHAT_NEXT);
            mHandlerThread.interrupt();
            mHandlerThread.quit();

            return unDealTaskList;
        }
    }

    private class SerialLoop implements Handler.Callback {

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case WHAT_NEXT:
                    try {
                        if (paused) break;
                        workingTask = mTasks.take();
                        workingTask.addFinishListener(finishCallback)
                                .start();
                    } catch (InterruptedException ignored) { }
                    break;
                default:
                    //ignored
            }
            return false;
        }
    }

    private static class SerialFinishCallback implements BaseDownloadTask.FinishListener {
        private final WeakReference<FileDownloadSerialQueue> mQueueWeakReference;

        SerialFinishCallback(WeakReference<FileDownloadSerialQueue> queueWeakReference) {
            this.mQueueWeakReference = queueWeakReference;
        }

        @Override
        public synchronized void over(BaseDownloadTask task) {
            task.removeFinishListener(this);

            if (mQueueWeakReference == null) {
                return;
            }

            final FileDownloadSerialQueue queue = mQueueWeakReference.get();
            if (queue == null) {
                return;
            }

            queue.workingTask = null;
            if (queue.paused) {
                return;
            }
            queue.sendNext();
        }
    }

    private void sendNext() {
        mHandler.sendEmptyMessage(WHAT_NEXT);
    }

}


/*
 * 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.util;

import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Environment;
import android.os.PowerManager;
import android.os.StatFs;
import android.text.TextUtils;

import com.liulishuo.filedownloader.BuildConfig;
import com.liulishuo.filedownloader.connection.FileDownloadConnection;
import com.liulishuo.filedownloader.download.CustomComponentHolder;
import com.liulishuo.filedownloader.exception.FileDownloadGiveUpRetryException;
import com.liulishuo.filedownloader.exception.FileDownloadSecurityException;
import com.liulishuo.filedownloader.model.FileDownloadModel;
import com.liulishuo.filedownloader.services.FileDownloadService;
import com.liulishuo.filedownloader.stream.FileDownloadOutputStream;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URLDecoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.liulishuo.filedownloader.model.FileDownloadModel.TOTAL_VALUE_IN_CHUNKED_RESOURCE;

/**
 * The utils for FileDownloader.
 */
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public class FileDownloadUtils {

    private static int minProgressStep = 65536;
    private static long minProgressTime = 2000;

    /**
     * @param minProgressStep The minimum bytes interval in per step to sync to the file and the
     *                        database.
     *

     *                        Used for adjudging whether is time to sync the downloaded so far bytes
     *                        to database and make sure sync the downloaded buffer to local file.
     *

     *                        More smaller more frequently, then download more slowly, but will more
     *                        safer in scene of the process is killed unexpected.
     *

     *                        Default 65536, which follow the value in
     *                        com.android.providers.downloads.Constants.
     * @see com.liulishuo.filedownloader.download.DownloadStatusCallback#onProgress(long)
     * @see #setMinProgressTime(long)
     */
    public static void setMinProgressStep(int minProgressStep) throws IllegalAccessException {
        if (isDownloaderProcess(FileDownloadHelper.getAppContext())) {
            FileDownloadUtils.minProgressStep = minProgressStep;
        } else {
            throw new IllegalAccessException("This value is used in the :filedownloader process,"
                    + " so set this value in your process is without effect. You can add "
                    + "'process.non-separate=true' in 'filedownloader.properties' to share the main"
                    + " process to FileDownloadService. Or you can configure this value in "
                    + "'filedownloader.properties' by 'download.min-progress-step'.");
        }
    }

    /**
     * @param minProgressTime The minimum millisecond interval in per time to sync to the file and
     *                        the database.
     *

     *                        Used for adjudging whether is time to sync the downloaded so far bytes
     *                        to database and make sure sync the downloaded buffer to local file.
     *

     *                        More smaller more frequently, then download more slowly, but will more
     *                        safer in scene of the process is killed unexpected.
     *

     *                        Default 2000, which follow the value in
     *                        com.android.providers.downloads.Constants.
     * @see com.liulishuo.filedownloader.download.DownloadStatusCallback#onProgress(long)
     * @see #setMinProgressStep(int)
     */
    public static void setMinProgressTime(long minProgressTime) throws IllegalAccessException {
        if (isDownloaderProcess(FileDownloadHelper.getAppContext())) {
            FileDownloadUtils.minProgressTime = minProgressTime;
        } else {
            throw new IllegalAccessException("This value is used in the :filedownloader process,"
                    + " so set this value in your process is without effect. You can add "
                    + "'process.non-separate=true' in 'filedownloader.properties' to share the main"
                    + " process to FileDownloadService. Or you can configure this value in "
                    + "'filedownloader.properties' by 'download.min-progress-time'.");
        }
    }

    public static int getMinProgressStep() {
        return minProgressStep;
    }

    public static long getMinProgressTime() {
        return minProgressTime;
    }

    /**
     * Checks whether the filename looks legitimate.
     */
    @SuppressWarnings({"SameReturnValue", "UnusedParameters"})
    public static boolean isFilenameValid(String filename) {
//        filename = filename.replaceFirst("/+", "/"); // normalize leading
        // slashes
//        return filename.startsWith(Environment.getDownloadCacheDirectory()
//                .toString())
//                || filename.startsWith(Environment
//                .getExternalStorageDirectory().toString());
        return true;
    }

    private static String defaultSaveRootPath;

    public static String getDefaultSaveRootPath() {
        if (!TextUtils.isEmpty(defaultSaveRootPath)) {
            return defaultSaveRootPath;
        }

        if (FileDownloadHelper.getAppContext().getExternalCacheDir() == null) {
            return Environment.getDownloadCacheDirectory().getAbsolutePath();
        } else {
            //noinspection ConstantConditions
            return FileDownloadHelper.getAppContext().getExternalCacheDir().getAbsolutePath();
        }
    }

    public static String getDefaultSaveFilePath(final String url) {
        return generateFilePath(getDefaultSaveRootPath(), generateFileName(url));
    }

    public static String generateFileName(final String url) {
        return md5(url);
    }

    /**
     * @see #getTargetFilePath(String, boolean, String)
     */
    public static String generateFilePath(String directory, String filename) {
        if (filename == null) {
            throw new IllegalStateException("can't generate real path, the file name is null");
        }

        if (directory == null) {
            throw new IllegalStateException("can't generate real path, the directory is null");
        }

        return formatString("%s%s%s", directory, File.separator, filename);
    }

    /**
     * The path is used as the default directory in the case of the task without set path.
     *
     * @param path default root path for save download file.
     * @see com.liulishuo.filedownloader.BaseDownloadTask#setPath(String, boolean)
     */
    public static void setDefaultSaveRootPath(final String path) {
        defaultSaveRootPath = path;
    }

    /**
     * @param targetPath The target path for the download task.
     * @return The temp path is {@code targetPath} in downloading status; The temp path is used for
     * storing the file not completed downloaded yet.
     */
    public static String getTempPath(final String targetPath) {
        return FileDownloadUtils.formatString("%s.temp", targetPath);
    }

    /**
     * @param url  The downloading URL.
     * @param path The absolute file path.
     * @return The download id.
     */
    public static int generateId(final String url, final String path) {
        return CustomComponentHolder.getImpl().getIdGeneratorInstance()
                .generateId(url, path, false);
    }

    /**
     * @param url  The downloading URL.
     * @param path If {@code pathAsDirectory} is {@code true}, {@code path} would be the absolute
     *             directory to place the file;
     *             If {@code pathAsDirectory} is {@code false}, {@code path} would be the absolute
     *             file path.
     * @return The download id.
     */
    public static int generateId(final String url, final String path,
                                 final boolean pathAsDirectory) {
        return CustomComponentHolder.getImpl().getIdGeneratorInstance()
                .generateId(url, path, pathAsDirectory);
    }

    public static String md5(String string) {
        byte[] hash;
        try {
            hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8"));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Huh, MD5 should be supported?", e);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Huh, UTF-8 should be supported?", e);
        }

        StringBuilder hex = new StringBuilder(hash.length * 2);
        for (byte b : hash) {
            if ((b & 0xFF) < 0x10) hex.append("0");
            hex.append(Integer.toHexString(b & 0xFF));
        }
        return hex.toString();
    }

    public static String getStack() {
        return getStack(true);
    }

    public static String getStack(final boolean printLine) {
        StackTraceElement[] stackTrace = new Throwable().getStackTrace();
        return getStack(stackTrace, printLine);
    }

    public static String getStack(final StackTraceElement[] stackTrace, final boolean printLine) {
        if ((stackTrace == null) || (stackTrace.length < 4)) {
            return "";
        }

        StringBuilder t = new StringBuilder();

        for (int i = 3; i < stackTrace.length; i++) {
            if (!stackTrace[i].getClassName().contains("com.liulishuo.filedownloader")) {
                continue;
            }
            t.append("[");
            t.append(stackTrace[i].getClassName()
                    .substring("com.liulishuo.filedownloader".length()));
            t.append(":");
            t.append(stackTrace[i].getMethodName());
            if (printLine) {
                t.append("(").append(stackTrace[i].getLineNumber()).append(")]");
            } else {
                t.append("]");
            }
        }
        return t.toString();
    }

    private static Boolean isDownloaderProcess;

    /**
     * @param context the context
     * @return {@code true} if the FileDownloadService is allowed to run on the current process,
     * {@code false} otherwise.
     */
    public static boolean isDownloaderProcess(final Context context) {
        if (isDownloaderProcess != null) {
            return isDownloaderProcess;
        }

        boolean result = false;
        do {
            if (FileDownloadProperties.getImpl().processNonSeparate) {
                result = true;
                break;
            }

            int pid = android.os.Process.myPid();
            final ActivityManager activityManager = (ActivityManager) context.
                    getSystemService(Context.ACTIVITY_SERVICE);

            if (activityManager == null) {
                FileDownloadLog.w(FileDownloadUtils.class, "fail to get the activity manager!");
                return false;
            }

            final List<ActivityManager.RunningAppProcessInfo> runningAppProcessInfoList =
                    activityManager.getRunningAppProcesses();

            if (null == runningAppProcessInfoList || runningAppProcessInfoList.isEmpty()) {
                FileDownloadLog
                        .w(FileDownloadUtils.class, "The running app process info list from"
                                + " ActivityManager is null or empty, maybe current App is not "
                                + "running.");
                return false;
            }

            for (ActivityManager.RunningAppProcessInfo processInfo : runningAppProcessInfoList) {
                if (processInfo.pid == pid) {
                    result = processInfo.processName.endsWith(":filedownloader");
                    break;
                }
            }

        } while (false);

        isDownloaderProcess = result;
        return isDownloaderProcess;
    }

    public static String[] convertHeaderString(final String nameAndValuesString) {
        final String[] lineString = nameAndValuesString.split("\n");
        final String[] namesAndValues = new String[lineString.length * 2];

        for (int i = 0; i < lineString.length; i++) {
            final String[] nameAndValue = lineString[i].split(": ");
            /**
             * @see Headers#toString()
             * @see Headers#name(int)
             * @see Headers#value(int)
             */
            namesAndValues[i * 2] = nameAndValue[0];
            namesAndValues[i * 2 + 1] = nameAndValue[1];
        }

        return namesAndValues;
    }

    public static long getFreeSpaceBytes(final String path) {
        long freeSpaceBytes;
        final StatFs statFs = new StatFs(path);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            freeSpaceBytes = statFs.getAvailableBytes();
        } else {
            //noinspection deprecation
            freeSpaceBytes = statFs.getAvailableBlocks() * (long) statFs.getBlockSize();
        }

        return freeSpaceBytes;
    }

    public static String formatString(final String msg, Object... args) {
        return String.format(Locale.ENGLISH, msg, args);
    }

    private static final String INTERNAL_DOCUMENT_NAME = "filedownloader";
    private static final String OLD_FILE_CONVERTED_FILE_NAME = ".old_file_converted";

    @SuppressWarnings("ResultOfMethodCallIgnored")
    public static void markConverted(final Context context) {
        final File file = getConvertedMarkedFile(context);
        try {
            file.getParentFile().mkdirs();
            file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static Boolean filenameConverted = null;

    /**
     * @return Whether has converted all files' name from 'filename'(in old architecture) to
     * 'filename.temp', if it's in downloading state.
     *

     * If {@code true}, You can check whether the file has completed downloading with
     * {@link File#exists()} directly.
     *

     * when {@link FileDownloadService#onCreate()} is invoked, This value will be assigned to
     * {@code true} only once since you upgrade the filedownloader version to 0.3.3 or higher.
     */
    public static boolean isFilenameConverted(final Context context) {
        if (filenameConverted == null) {
            filenameConverted = getConvertedMarkedFile(context).exists();
        }

        return filenameConverted;
    }

    public static File getConvertedMarkedFile(final Context context) {
        return new File(context.getFilesDir().getAbsolutePath() + File.separator
                + INTERNAL_DOCUMENT_NAME, OLD_FILE_CONVERTED_FILE_NAME);
    }

    // note on https://tools.ietf.org/html/rfc5987
    private static final Pattern CONTENT_DISPOSITION_WITH_ASTERISK_PATTERN =
            Pattern.compile("attachment;\\s*filename\\*\\s*=\\s*\"*([^\"]*)'\\S*'([^\"]*)\"*");
    // note on http://www.ietf.org/rfc/rfc1806.txt
    private static final Pattern CONTENT_DISPOSITION_WITHOUT_ASTERISK_PATTERN =
            Pattern.compile("attachment;\\s*filename\\s*=\\s*\"*([^\"\\n]*)\"*");

    public static long parseContentRangeFoInstanceLength(String contentRange) {
        if (contentRange == null) return -1;

        final String[] session = contentRange.split("/");
        if (session.length >= 2) {
            try {
                return Long.parseLong(session[1]);
            } catch (NumberFormatException e) {
                FileDownloadLog.w(FileDownloadUtils.class, "parse instance length failed with %s",
                        contentRange);
            }
        }

        return -1;
    }

    /**
     * The same to com.android.providers.downloads.Helpers#parseContentDisposition.
     *

     * Parse the Content-Disposition HTTP Header. The format of the header
     * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
     * This header provides a filename for content that is going to be
     * downloaded to the file system. We only support the attachment type.
     */
    public static String parseContentDisposition(String contentDisposition) {
        if (contentDisposition == null) {
            return null;
        }

        try {
            Matcher m = CONTENT_DISPOSITION_WITH_ASTERISK_PATTERN.matcher(contentDisposition);
            if (m.find()) {
                String charset = m.group(1);
                String encodeFileName = m.group(2);
                return URLDecoder.decode(encodeFileName, charset);
            }

            m = CONTENT_DISPOSITION_WITHOUT_ASTERISK_PATTERN.matcher(contentDisposition);
            if (m.find()) {
                return m.group(1);
            }
        } catch (IllegalStateException | UnsupportedEncodingException ignore) {
            // This function is defined as returning null when it can't parse the header
        }
        return null;
    }

    /**
     * @param path            If {@code pathAsDirectory} is true, the {@code path} would be the
     *                        absolute directory to settle down the file;
     *                        If {@code pathAsDirectory} is false, the {@code path} would be the
     *                        absolute file path.
     * @param pathAsDirectory whether the {@code path} is a directory.
     * @param filename        the file's name.
     * @return the absolute path of the file. If can't find by params, will return {@code null}.
     */
    public static String getTargetFilePath(String path, boolean pathAsDirectory, String filename) {
        if (path == null) {
            return null;
        }

        if (pathAsDirectory) {
            if (filename == null) {
                return null;
            }

            return FileDownloadUtils.generateFilePath(path, filename);
        } else {
            return path;
        }
    }

    /**
     * The same to {@link File#getParent()}, for non-creating a file object.
     *
     * @return this file's parent pathname or {@code null}.
     */
    public static String getParent(final String path) {
        int length = path.length(), firstInPath = 0;
        if (File.separatorChar == '\\' && length > 2 && path.charAt(1) == ':') {
            firstInPath = 2;
        }
        int index = path.lastIndexOf(File.separatorChar);
        if (index == -1 && firstInPath > 0) {
            index = 2;
        }
        if (index == -1 || path.charAt(length - 1) == File.separatorChar) {
            return null;
        }
        if (path.indexOf(File.separatorChar) == index
                && path.charAt(firstInPath) == File.separatorChar) {
            return path.substring(0, index + 1);
        }
        return path.substring(0, index);
    }

    private static final String FILEDOWNLOADER_PREFIX = "FileDownloader";

    public static String getThreadPoolName(String name) {
        return FILEDOWNLOADER_PREFIX + "-" + name;
    }

    public static boolean isNetworkNotOnWifiType() {
        final ConnectivityManager manager = (ConnectivityManager) FileDownloadHelper.getAppContext()
                .
                        getSystemService(Context.CONNECTIVITY_SERVICE);

        if (manager == null) {
            FileDownloadLog.w(FileDownloadUtils.class, "failed to get connectivity manager!");
            return true;
        }

        //noinspection MissingPermission, because we check permission accessable when invoked
        final NetworkInfo info = manager.getActiveNetworkInfo();

        return info == null || info.getType() != ConnectivityManager.TYPE_WIFI;
    }

    public static boolean checkPermission(String permission) {
        final int perm = FileDownloadHelper.getAppContext()
                .checkCallingOrSelfPermission(permission);
        return perm == PackageManager.PERMISSION_GRANTED;
    }

    public static long convertContentLengthString(String s) {
        if (s == null) return -1;
        try {
            return Long.parseLong(s);
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    public static String findEtag(final int id, FileDownloadConnection connection) {
        if (connection == null) {
            throw new RuntimeException("connection is null when findEtag");
        }

        final String newEtag = connection.getResponseHeaderField("Etag");

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(FileDownloadUtils.class, "etag find %s for task(%d)", newEtag, id);
        }

        return newEtag;
    }

    // accept range is effect by  response code and Accept-Ranges header field.
    public static boolean isAcceptRange(int responseCode, FileDownloadConnection connection) {
        if (responseCode == HttpURLConnection.HTTP_PARTIAL
                || responseCode == FileDownloadConnection.RESPONSE_CODE_FROM_OFFSET) return true;

        final String acceptRanges = connection.getResponseHeaderField("Accept-Ranges");
        return "bytes".equals(acceptRanges);
    }

    // because of we using one of two HEAD method to request or using range:0-0 to trial connection
    // only if connection api not support, so we test content-range first and then test
    // content-length.
    public static long findInstanceLengthForTrial(FileDownloadConnection connection) {
        long length = findInstanceLengthFromContentRange(connection);
        if (length < 0) {
            length = TOTAL_VALUE_IN_CHUNKED_RESOURCE;
            FileDownloadLog.w(FileDownloadUtils.class, "don't get instance length from"
                    + "Content-Range header");
        }
        // the response of HEAD method is not very canonical sometimes(it depends on server
        // implementation)
        // so that it's uncertain the content-length is the same as the response of GET method if
        // content-length=0, so we have to filter this case in here.
        if (length == 0 && FileDownloadProperties.getImpl().trialConnectionHeadMethod) {
            length = TOTAL_VALUE_IN_CHUNKED_RESOURCE;
        }

        return length;
    }

    public static long findInstanceLengthFromContentRange(FileDownloadConnection connection) {
        return parseContentRangeFoInstanceLength(getContentRangeHeader(connection));
    }

    private static String getContentRangeHeader(FileDownloadConnection connection) {
        return connection.getResponseHeaderField("Content-Range");
    }

    public static long findContentLength(final int id, FileDownloadConnection connection) {
        long contentLength = convertContentLengthString(
                connection.getResponseHeaderField("Content-Length"));
        final String transferEncoding = connection.getResponseHeaderField("Transfer-Encoding");

        if (contentLength < 0) {
            final boolean isEncodingChunked = transferEncoding != null && transferEncoding
                    .equals("chunked");
            if (!isEncodingChunked) {
                // not chunked transfer encoding data
                if (FileDownloadProperties.getImpl().httpLenient) {
                    // do not response content-length either not chunk transfer encoding,
                    // but HTTP lenient is true, so handle as the case of transfer encoding chunk
                    contentLength = TOTAL_VALUE_IN_CHUNKED_RESOURCE;
                    if (FileDownloadLog.NEED_LOG) {
                        FileDownloadLog
                                .d(FileDownloadUtils.class, "%d response header is not legal but "
                                        + "HTTP lenient is true, so handle as the case of "
                                        + "transfer encoding chunk", id);
                    }
                } else {
                    throw new FileDownloadGiveUpRetryException("can't know the size of the "
                            + "download file, and its Transfer-Encoding is not Chunked "
                            + "either.\nyou can ignore such exception by add "
                            + "http.lenient=true to the filedownloader.properties");
                }
            } else {
                contentLength = TOTAL_VALUE_IN_CHUNKED_RESOURCE;
            }
        }

        return contentLength;
    }

    public static long findContentLengthFromContentRange(FileDownloadConnection connection) {
        final String contentRange = getContentRangeHeader(connection);
        long contentLength = parseContentLengthFromContentRange(contentRange);
        if (contentLength < 0) contentLength = TOTAL_VALUE_IN_CHUNKED_RESOURCE;
        return contentLength;
    }

    public static long parseContentLengthFromContentRange(String contentRange) {
        if (contentRange == null || contentRange.length() == 0) return -1;
        final String pattern = "bytes (\\d+)-(\\d+)/\\d+";
        try {
            final Pattern r = Pattern.compile(pattern);
            final Matcher m = r.matcher(contentRange);
            if (m.find()) {
                final long rangeStart = Long.parseLong(m.group(1));
                final long rangeEnd = Long.parseLong(m.group(2));
                return rangeEnd - rangeStart + 1;
            }
        } catch (Exception e) {
            FileDownloadLog.e(FileDownloadUtils.class, e, "parse content length"
                    + " from content range error");
        }
        return -1;
    }

    public static String findFilename(FileDownloadConnection connection, String url)
            throws FileDownloadSecurityException {
        String filename = FileDownloadUtils.parseContentDisposition(connection.
                getResponseHeaderField("Content-Disposition"));

        if (TextUtils.isEmpty(filename)) {
            filename = FileDownloadUtils.generateFileName(url);
        } else if (filename.contains("../")) {
            throw new FileDownloadSecurityException(FileDownloadUtils.formatString(
                    "The filename [%s] from the response is not allowable, because it contains "
                            + "'../', which can raise the directory traversal vulnerability",
                    filename));
        }

        return filename;
    }

    public static FileDownloadOutputStream createOutputStream(final String path)
            throws IOException {

        if (TextUtils.isEmpty(path)) {
            throw new RuntimeException("found invalid internal destination path, empty");
        }

        //noinspection ConstantConditions
        if (!FileDownloadUtils.isFilenameValid(path)) {
            throw new RuntimeException(
                    FileDownloadUtils.formatString("found invalid internal destination filename"
                            + " %s", path));
        }

        File file = new File(path);

        if (file.exists() && file.isDirectory()) {
            throw new RuntimeException(
                    FileDownloadUtils.formatString("found invalid internal destination path[%s],"
                            + " & path is directory[%B]", path, file.isDirectory()));
        }
        if (!file.exists()) {
            if (!file.createNewFile()) {
                throw new IOException(
                        FileDownloadUtils.formatString("create new file error  %s",
                                file.getAbsolutePath()));
            }
        }

        return CustomComponentHolder.getImpl().createOutputStream(file);
    }

    public static boolean isBreakpointAvailable(final int id, final FileDownloadModel model) {
        return isBreakpointAvailable(id, model, null);
    }

    /**
     * @return can resume by break point
     */
    public static boolean isBreakpointAvailable(final int id, final FileDownloadModel model,
                                                final Boolean outputStreamSupportSeek) {
        if (model == null) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(FileDownloadUtils.class, "can't continue %d model == null", id);
            }
            return false;
        }

        if (model.getTempFilePath() == null) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog
                        .d(FileDownloadUtils.class, "can't continue %d temp path == null", id);
            }
            return false;
        }

        return isBreakpointAvailable(id, model, model.getTempFilePath(), outputStreamSupportSeek);
    }

    public static boolean isBreakpointAvailable(final int id, final FileDownloadModel model,
                                                final String path,
                                                final Boolean outputStreamSupportSeek) {
        boolean result = false;

        do {
            if (path == null) {
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(FileDownloadUtils.class, "can't continue %d path = null", id);
                }
                break;
            }

            File file = new File(path);
            final boolean isExists = file.exists();
            final boolean isDirectory = file.isDirectory();

            if (!isExists || isDirectory) {
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(FileDownloadUtils.class,
                            "can't continue %d file not suit, exists[%B], directory[%B]",
                            id, isExists, isDirectory);
                }
                break;
            }

            final long fileLength = file.length();
            final long currentOffset = model.getSoFar();

            if (model.getConnectionCount() <= 1 && currentOffset == 0) {
                // the sofar is stored on connection table
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(FileDownloadUtils.class,
                            "can't continue %d the downloaded-record is zero.",
                            id);
                }
                break;
            }

            final long totalLength = model.getTotal();
            if (fileLength < currentOffset
                    || (totalLength != TOTAL_VALUE_IN_CHUNKED_RESOURCE  // not chunk transfer
                    && (fileLength > totalLength || currentOffset >= totalLength))
                    ) {
                // dirty data.
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(FileDownloadUtils.class, "can't continue %d dirty data"
                                    + " fileLength[%d] sofar[%d] total[%d]",
                            id, fileLength, currentOffset, totalLength);
                }
                break;
            }

            if (outputStreamSupportSeek != null && !outputStreamSupportSeek
                    && totalLength == fileLength) {
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(FileDownloadUtils.class, "can't continue %d, because of the "
                                    + "output stream doesn't support seek, but the task has "
                                    + "already pre-allocated, so we only can download it from the"
                                    + " very beginning.",
                            id);
                }
                break;
            }

            result = true;
        } while (false);

        return result;
    }

    public static void deleteTaskFiles(String targetFilepath, String tempFilePath) {
        deleteTempFile(tempFilePath);
        deleteTargetFile(targetFilepath);
    }

    public static void deleteTempFile(String tempFilePath) {
        if (tempFilePath != null) {
            final File tempFile = new File(tempFilePath);
            if (tempFile.exists()) {
                //noinspection ResultOfMethodCallIgnored
                tempFile.delete();
            }
        }
    }

    public static void deleteTargetFile(String targetFilePath) {
        if (targetFilePath != null) {
            final File targetFile = new File(targetFilePath);
            if (targetFile.exists()) {
                //noinspection ResultOfMethodCallIgnored
                targetFile.delete();
            }
        }
    }

    public static boolean isNeedSync(long bytesDelta, long timestampDelta) {
        return bytesDelta > FileDownloadUtils.getMinProgressStep()
                && timestampDelta > FileDownloadUtils.getMinProgressTime();
    }

    public static String defaultUserAgent() {
        return formatString("FileDownloader/%s", BuildConfig.VERSION_NAME);
    }

    private static boolean isAppOnForeground(Context context) {
        ActivityManager activityManager = (ActivityManager) context.getApplicationContext()
                .getSystemService(Context.ACTIVITY_SERVICE);
        if (activityManager == null) return false;

        List<ActivityManager.RunningAppProcessInfo> appProcesses =
                activityManager.getRunningAppProcesses();
        if (appProcesses == null) return false;

        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        if (pm == null) return false;
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
            if (!pm.isInteractive()) return false;
        } else {
            if (!pm.isScreenOn()) return false;
        }

        String packageName = context.getApplicationContext().getPackageName();
        for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
            // The name of the process that this object is associated with.
            if (appProcess.processName.equals(packageName) && appProcess.importance
                    == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                return true;
            }

        }
        return false;
    }

    public static boolean needMakeServiceForeground(Context context) {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isAppOnForeground(context);
    }
}