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;
    }
} public class FileDownloadUrlConnection implements FileDownloadConnection {
    protected URLConnection mConnection;

    public FileDownloadUrlConnection(String originUrl, Configuration configuration)
            throws IOException {
        this(new URL(originUrl), configuration);
    }

    public FileDownloadUrlConnection(URL url, Configuration configuration) throws IOException {
        if (configuration != null && configuration.proxy != null) {
            mConnection = url.openConnection(configuration.proxy);
        } else {
            mConnection = url.openConnection();
        }
        if (mConnection instanceof HttpURLConnection) {
            ((HttpURLConnection) mConnection).setInstanceFollowRedirects(false);
        }

        if (configuration != null) {
            if (configuration.readTimeout != null) {
                mConnection.setReadTimeout(configuration.readTimeout);
            }

            if (configuration.connectTimeout != null) {
                mConnection.setConnectTimeout(configuration.connectTimeout);
            }
        }
    }

    public FileDownloadUrlConnection(String originUrl) throws IOException {
        this(originUrl, null);
    }

    @Override
    public void addHeader(String name, String value) {
        mConnection.addRequestProperty(name, value);
    }

    @Override
    public boolean dispatchAddResumeOffset(String etag, long offset) {
        return false;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return mConnection.getInputStream();
    }

    @Override
    public Map<String, List<String>> getRequestHeaderFields() {
        return mConnection.getRequestProperties();
    }

    @Override
    public Map<String, List<String>> getResponseHeaderFields() {
        return mConnection.getHeaderFields();
    }

    @Override
    public String getResponseHeaderField(String name) {
        return mConnection.getHeaderField(name);
    }

    @Override public boolean setRequestMethod(String method) throws ProtocolException {
        if (mConnection instanceof HttpURLConnection) {
            ((HttpURLConnection) mConnection).setRequestMethod(method);
            return true;
        }

        return false;
    }

    @Override
    public void execute() throws IOException {
        mConnection.connect();
    }

    @Override
    public int getResponseCode() throws IOException {
        if (mConnection instanceof HttpURLConnection) {
            return ((HttpURLConnection) mConnection).getResponseCode();
        }

        return FileDownloadConnection.NO_RESPONSE_CODE;
    }

    @Override
    public void ending() {
        try {
            mConnection.getInputStream().close();
        } catch (IOException ignored) {
        }
    }


    public static class Creator implements FileDownloadHelper.ConnectionCreator {
        private final Configuration mConfiguration;

        public Creator() {
            this(null);
        }

        public Creator(Configuration configuration) {
            this.mConfiguration = configuration;
        }

        FileDownloadConnection create(URL url) throws IOException {
            return new FileDownloadUrlConnection(url, mConfiguration);
        }

        @Override
        public FileDownloadConnection create(String originUrl) throws IOException {
            return new FileDownloadUrlConnection(originUrl, mConfiguration);
        }
    }

    /**
     * The sample configuration for the {@link FileDownloadUrlConnection}
     */
    public static class Configuration {
        private Proxy proxy;
        private Integer readTimeout;
        private Integer connectTimeout;

        /**
         * The connection will be made through the specified proxy.
         * <p>
         * This {@code proxy} will be used when invoke {@link URL#openConnection(Proxy)}
         *
         * @param proxy the proxy will be applied to the {@link FileDownloadUrlConnection}
         */
        public Configuration proxy(Proxy proxy) {
            this.proxy = proxy;
            return this;
        }

        /**
         * Sets the read timeout to a specified timeout, in milliseconds. A non-zero value specifies
         * the timeout when reading from Input stream when a connection is established to a resource
         * <p>
         * If the timeout expires before there is data available for read, a
         * java.net.SocketTimeoutException is raised. A timeout of zero is interpreted as an
         * infinite timeout.
         * <p>
         * This {@code readTimeout} will be applied through
         * {@link URLConnection#setReadTimeout(int)}
         *
         * @param readTimeout an <code>int</code> that specifies the timeout value to be used in
         *                    milliseconds
         */
        public Configuration readTimeout(int readTimeout) {
            this.readTimeout = readTimeout;
            return this;
        }

        /**
         * Sets a specified timeout value, in milliseconds, to be used when opening a communications
         * link to the resource referenced by this URLConnection.  If the timeout expires before the
         * connection can be established, a java.net.SocketTimeoutException is raised. A timeout of
         * zero is interpreted as an infinite timeout.
         * <p>
         * This {@code connectionTimeout} will be applied through
         * {@link URLConnection#setConnectTimeout(int)}
         *
         * @param connectTimeout an <code>int</code> that specifies the connect timeout value in
         *                       milliseconds
         */
        public Configuration connectTimeout(int connectTimeout) {
            this.connectTimeout = connectTimeout;
            return this;
        }


    }
} @SuppressWarnings("UnusedParameters")
public interface FileDownloadDatabase {

    /**
     * Invoked when task is started.
     *
     * @param id the download id.
     */
    void onTaskStart(final int id);

    /**
     * Find the model which identify is {@code id}.
     *
     * @param id the download id.
     */
    FileDownloadModel find(final int id);

    /**
     * Find the connection model which download identify is {@code id}
     *
     * @param id the download id.
     */
    List<ConnectionModel> findConnectionModel(int id);

    /**
     * Delete all connection model store on database through the download id.
     *
     * @param id the download id.
     */
    void removeConnections(int id);

    /**
     * Insert the {@code model} to connection table.
     *
     * @param model the connection model.
     */
    void insertConnectionModel(ConnectionModel model);

    /**
     * Update the currentOffset with {@code currentOffset} which id is {@code id}, index is
     * {@code index}
     *
     * @param id            the download id.
     * @param index         the connection index.
     * @param currentOffset the current offset.
     */
    void updateConnectionModel(int id, int index, long currentOffset);

    /**
     * Update the count of connection.
     *
     * @param count the connection count.
     */
    void updateConnectionCount(int id, int count);

    /**
     * Insert the model to the database.
     *
     * @param downloadModel the download model.
     */
    void insert(final FileDownloadModel downloadModel);

    /**
     * Update the data compare to the {@code downloadModel}
     *
     * @param downloadModel the download model.
     */
    void update(final FileDownloadModel downloadModel);

    /**
     * Remove the model which identify is {@code id}.
     *
     * @param id the download id.
     * @return {@code true} if succeed to remove model from the database.
     */
    boolean remove(final int id);

    /**
     * Clear all models in this database.
     */
    void clear();


    /**
     * Update when the old one is overdue.
     */
    void updateOldEtagOverdue(int id, String newEtag, long sofar, long total, int connectionCount);

    /**
     * Update the data because of the download status alternative to
     * {@link FileDownloadStatus#connected}.
     *
     * @param id       the download id.
     * @param total    the new total bytes.
     * @param etag     the new etag. this value will be {@code null} when we can't find it on
     *                 response header.
     * @param filename the new file name. this value will be {@code null} when its no need to store.
     */
    void updateConnected(int id, long total, String etag, String filename);

    /**
     * Update the sofar bytes with the status {@code progress}, so don't forget to store the
     * {@link FileDownloadStatus#progress} too.
     *
     * @param sofarBytes the current sofar bytes.
     */
    void updateProgress(int id, long sofarBytes);

    /**
     * Update the data because of the download status alternative to
     * {@link FileDownloadStatus#error}.
     *
     * @param id        the download id.
     * @param throwable the new exception.
     * @param sofar     the new so far bytes.
     */
    void updateError(int id, Throwable throwable, long sofar);

    /**
     * Update the data because of the download status alternative to
     * {@link FileDownloadStatus#retry}.
     *
     * @param id        the download id.
     * @param throwable the new exception.
     */
    void updateRetry(int id, Throwable throwable);

    /**
     * Update the data because of the download status alternative to
     * {@link FileDownloadStatus#completed}.
     * The latest version will remove model from DB.
     *
     * @param id    the download id.
     * @param total the new total bytes.
     */
    void updateCompleted(int id, final long total);

    /**
     * Update the data because of the download status alternative to
     * {@link FileDownloadStatus#paused}.
     *
     * @param id    the download id.
     * @param sofar the new so far bytes.
     */
    void updatePause(int id, final long sofar);

    /**
     * Update the data because of the download status alternative to
     * {@link FileDownloadStatus#pending}.
     *
     * @param id the download id.
     */
    void updatePending(int id);

    /**
     * Get the maintainer for the database, this maintainer will be used when the database is
     * initializing.
     * <p>
     * The maintainer will return all data on the database.
     * <p>
     * Demo: {@link SqliteDatabaseImpl.Maintainer}
     *
     * @return the maintainer for maintain the database.
     */
    Maintainer maintainer();

    /**
     * the maintainer for the database, this maintainer will be used when the database is
     * initializing.
     */
    @SuppressWarnings("EmptyMethod")
    interface Maintainer extends Iterable<FileDownloadModel> {
        /**
         * invoke this method when the operation for maintain is finished.
         */
        void onFinishMaintain();

        /**
         * invoke this method when the {@code model} is invalid and has been removed.
         *
         * @param model the removed invalid model.
         */
        void onRemovedInvalidData(FileDownloadModel model);

        /**
         * invoke this method when the {@code model} is valid to save and has been refreshed.
         *
         * @param model the refreshed valid model.
         */
        void onRefreshedValidData(FileDownloadModel model);

        /**
         * invoke this method when the {@link FileDownloadModel#id} is changed because of the
         * different {@link com.liulishuo.filedownloader.util.FileDownloadHelper.IdGenerator},
         * which generate the new id for the task.
         * <p>
         * tips: you need to update the filedownloader-table and the connection-table.
         *
         * @param oldId          the old id for the {@code modelWithNewId}
         * @param modelWithNewId the model with the new id.
         */
        void changeFileDownloadModelId(int oldId, FileDownloadModel modelWithNewId);
    }
} public class RemitDatabase implements FileDownloadDatabase {

    private final NoDatabaseImpl cachedDatabase;
    private final SqliteDatabaseImpl realDatabase;


    private Handler handler;
    private final long minInterval;

    private final List<Integer> freeToDBIdList = new ArrayList<>();
    private AtomicInteger handlingId = new AtomicInteger();
    private volatile Thread parkThread;

    private static final int WHAT_CLEAN_LOCK = 0;

    public RemitDatabase() {
        this.cachedDatabase = new NoDatabaseImpl();
        this.realDatabase = new SqliteDatabaseImpl();
        this.minInterval = FileDownloadProperties.getImpl().downloadMinProgressTime;

        final HandlerThread thread = new HandlerThread(
                FileDownloadUtils.getThreadPoolName("RemitHandoverToDB"));
        thread.start();
        handler = new Handler(thread.getLooper(), new Handler.Callback() {
            @Override public boolean handleMessage(Message msg) {
                final int id = msg.what;
                if (id == WHAT_CLEAN_LOCK) {
                    if (parkThread != null) {
                        LockSupport.unpark(parkThread);
                        parkThread = null;
                    }
                    return false;
                }

                try {
                    handlingId.set(id);

                    syncCacheToDB(id);
                    freeToDBIdList.add(id);
                } finally {
                    handlingId.set(0);
                    if (parkThread != null) {
                        LockSupport.unpark(parkThread);
                        parkThread = null;
                    }
                }

                return false;
            }
        });
    }

    private void syncCacheToDB(int id) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "sync cache to db %d", id);
        }

        // need to attention that the `sofar` in `FileDownloadModel` database will
        // be updated even through this is a multiple connections task. But the
        // multi-connections task only concern about the `ConnectionModel` database,
        // so it doesn't matter in current.
        realDatabase.update(cachedDatabase.find(id));
        final List<ConnectionModel> modelList = cachedDatabase.findConnectionModel(id);
        realDatabase.removeConnections(id);
        for (ConnectionModel connectionModel : modelList) {
            realDatabase.insertConnectionModel(connectionModel);
        }
    }

    private boolean isNoNeedUpdateToRealDB(int id) {
        return !freeToDBIdList.contains(id);
    }

    @Override public void onTaskStart(int id) {
        handler.sendEmptyMessageDelayed(id, minInterval);
    }

    @Override public FileDownloadModel find(int id) {
        return this.cachedDatabase.find(id);
    }

    @Override public List<ConnectionModel> findConnectionModel(int id) {
        return this.cachedDatabase.findConnectionModel(id);
    }

    @Override public void removeConnections(int id) {
        this.cachedDatabase.removeConnections(id);
        if (isNoNeedUpdateToRealDB(id)) return;
        this.realDatabase.removeConnections(id);

    }

    @Override public void insertConnectionModel(ConnectionModel model) {
        this.cachedDatabase.insertConnectionModel(model);
        final int id = model.getId();
        if (isNoNeedUpdateToRealDB(id)) return;
        this.realDatabase.insertConnectionModel(model);
    }

    @Override public void updateConnectionModel(int id, int index, long currentOffset) {
        this.cachedDatabase.updateConnectionModel(id, index, currentOffset);

        if (isNoNeedUpdateToRealDB(id)) return;
        this.realDatabase.updateConnectionModel(id, index, currentOffset);
    }

    @Override public void updateProgress(int id, long sofarBytes) {
        this.cachedDatabase.updateProgress(id, sofarBytes);
        if (isNoNeedUpdateToRealDB(id)) return;
        this.realDatabase.updateProgress(id, sofarBytes);
    }

    @Override public void updateConnectionCount(int id, int count) {
        this.cachedDatabase.updateConnectionCount(id, count);
        if (isNoNeedUpdateToRealDB(id)) return;
        this.realDatabase.updateConnectionCount(id, count);

    }

    @Override public void insert(FileDownloadModel downloadModel) {
        this.cachedDatabase.insert(downloadModel);
        if (isNoNeedUpdateToRealDB(downloadModel.getId())) return;
        this.realDatabase.insert(downloadModel);
    }

    @Override public void update(FileDownloadModel downloadModel) {
        this.cachedDatabase.update(downloadModel);
        if (isNoNeedUpdateToRealDB(downloadModel.getId())) return;
        this.realDatabase.update(downloadModel);
    }

    @Override public boolean remove(int id) {
        this.realDatabase.remove(id);
        return this.cachedDatabase.remove(id);
    }

    @Override public void clear() {
        this.cachedDatabase.clear();
        this.realDatabase.clear();
    }

    @Override public void updateOldEtagOverdue(int id, String newEtag, long sofar, long total,
                                               int connectionCount) {
        this.cachedDatabase.updateOldEtagOverdue(id, newEtag, sofar, total, connectionCount);
        if (isNoNeedUpdateToRealDB(id)) return;
        this.realDatabase.updateOldEtagOverdue(id, newEtag, sofar, total, connectionCount);
    }

    @Override public void updateConnected(int id, long total, String etag, String filename) {
        this.cachedDatabase.updateConnected(id, total, etag, filename);
        if (isNoNeedUpdateToRealDB(id)) return;
        this.realDatabase.updateConnected(id, total, etag, filename);
    }

    @Override public void updatePending(int id) {
        this.cachedDatabase.updatePending(id);
        if (isNoNeedUpdateToRealDB(id)) return;
        this.realDatabase.updatePending(id);
    }

    @Override public void updateRetry(int id, Throwable throwable) {
        this.cachedDatabase.updateRetry(id, throwable);
        if (isNoNeedUpdateToRealDB(id)) return;
        this.realDatabase.updateRetry(id, throwable);
    }

    private void ensureCacheToDB(int id) {
        handler.removeMessages(id);
        if (handlingId.get() == id) {
            parkThread = Thread.currentThread();
            handler.sendEmptyMessage(WHAT_CLEAN_LOCK);
            LockSupport.park();
        } else {
            syncCacheToDB(id);
        }
    }

    @Override public void updateError(int id, Throwable throwable, long sofar) {
        this.cachedDatabase.updateError(id, throwable, sofar);
        if (isNoNeedUpdateToRealDB(id)) {
            ensureCacheToDB(id);
        }
        this.realDatabase.updateError(id, throwable, sofar);
        freeToDBIdList.remove((Integer) id);
    }

    @Override public void updateCompleted(int id, long total) {
        this.cachedDatabase.updateCompleted(id, total);
        if (isNoNeedUpdateToRealDB(id)) {
            handler.removeMessages(id);
            if (handlingId.get() == id) {
                parkThread = Thread.currentThread();
                handler.sendEmptyMessage(WHAT_CLEAN_LOCK);
                LockSupport.park();
                this.realDatabase.updateCompleted(id, total);
            }
        } else {
            this.realDatabase.updateCompleted(id, total);
        }
        freeToDBIdList.remove((Integer) id);
    }

    @Override public void updatePause(int id, long sofar) {
        this.cachedDatabase.updatePause(id, sofar);
        if (isNoNeedUpdateToRealDB(id)) {
            ensureCacheToDB(id);
        }
        this.realDatabase.updatePause(id, sofar);
        freeToDBIdList.remove((Integer) id);
    }


    @Override public Maintainer maintainer() {
        return this.realDatabase.maintainer(this.cachedDatabase.downloaderModelMap,
                this.cachedDatabase.connectionModelListMap);
    }

    public static class Maker implements FileDownloadHelper.DatabaseCustomMaker {

        @Override public FileDownloadDatabase customMake() {
            return new RemitDatabase();
        }
    }
} @SuppressWarnings("WeakerAccess")
public class FileDownloadModel implements Parcelable {

    public static final int TOTAL_VALUE_IN_CHUNKED_RESOURCE = -1;
    public static final int DEFAULT_CALLBACK_PROGRESS_TIMES = 100;

    // download id
    private int id;
    public static final String ID = "_id";

    // download url
    private String url;
    public static final String URL = "url";

    // save path
    private String path;
    public static final String PATH = "path";


    private boolean pathAsDirectory;
    public static final String PATH_AS_DIRECTORY = "pathAsDirectory";

    private String filename;
    public static final String FILENAME = "filename";

    private final AtomicInteger status;
    public static final String STATUS = "status";

    private final AtomicLong soFar;
    private long total;

    public static final String SOFAR = "sofar";
    public static final String TOTAL = "total";

    private String errMsg;
    public static final String ERR_MSG = "errMsg";

    // header
    private String eTag;
    public static final String ETAG = "etag";

    private int connectionCount;
    public static final String CONNECTION_COUNT = "connectionCount";

    public void setId(int id) {
        this.id = id;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void setPath(String path, boolean pathAsDirectory) {
        this.path = path;
        this.pathAsDirectory = pathAsDirectory;
    }

    public void setStatus(byte status) {
        this.status.set(status);
    }

    public void setSoFar(long soFar) {
        this.soFar.set(soFar);
    }

    public void increaseSoFar(long increaseBytes) {
        this.soFar.addAndGet(increaseBytes);
    }

    public void setTotal(long total) {
        this.isLargeFile = total > Integer.MAX_VALUE;
        this.total = total;
    }

    public int getId() {
        return id;
    }

    public String getUrl() {
        return url;
    }

    /**
     * Get the path user set from {@link BaseDownloadTask#setPath(String)}
     *
     * @return the path user set from {@link BaseDownloadTask#setPath(String)}
     * @see #getTargetFilePath()
     */
    public String getPath() {
        return path;
    }

    /**
     * Get the finally target file path is used for store the download file.
     * <p/>
     * This path is composited with {@link #path}、{@link #pathAsDirectory}、{@link #filename}.
     * <p/>
     * Why {@link #getPath()} may be not equal to getTargetFilePath()? this case only occurred
     * when the {@link #isPathAsDirectory()} is {@code true}, on this scenario the
     * {@link #getPath()} is directory, and the getTargetFilePath() is 'directory + "/" + filename'.
     *
     * @return the finally target file path.
     */
    public String getTargetFilePath() {
        return FileDownloadUtils.getTargetFilePath(getPath(), isPathAsDirectory(), getFilename());
    }

    public String getTempFilePath() {
        if (getTargetFilePath() == null) {
            return null;
        }
        return FileDownloadUtils.getTempPath(getTargetFilePath());
    }

    public byte getStatus() {
        return (byte) status.get();
    }

    public long getSoFar() {
        return soFar.get();
    }

    public long getTotal() {
        return total;
    }

    public boolean isChunked() {
        return total == TOTAL_VALUE_IN_CHUNKED_RESOURCE;
    }

    public String getETag() {
        return eTag;
    }

    public void setETag(String eTag) {
        this.eTag = eTag;
    }

    public String getErrMsg() {
        return errMsg;
    }

    public void setErrMsg(String errMsg) {
        this.errMsg = errMsg;
    }

    public void setFilename(String filename) {
        this.filename = filename;
    }

    public boolean isPathAsDirectory() {
        return pathAsDirectory;
    }

    public String getFilename() {
        return filename;
    }

    public void setConnectionCount(int connectionCount) {
        this.connectionCount = connectionCount;
    }

    public int getConnectionCount() {
        return connectionCount;
    }

    /**
     * reset the connection count to default value: 1.
     */
    public void resetConnectionCount() {
        this.connectionCount = 1;
    }

    public ContentValues toContentValues() {
        ContentValues cv = new ContentValues();
        cv.put(ID, getId());
        cv.put(URL, getUrl());
        cv.put(PATH, getPath());
        cv.put(STATUS, getStatus());
        cv.put(SOFAR, getSoFar());
        cv.put(TOTAL, getTotal());
        cv.put(ERR_MSG, getErrMsg());
        cv.put(ETAG, getETag());
        cv.put(CONNECTION_COUNT, getConnectionCount());
        cv.put(PATH_AS_DIRECTORY, isPathAsDirectory());
        if (isPathAsDirectory() && getFilename() != null) {
            cv.put(FILENAME, getFilename());
        }

        return cv;
    }


    private boolean isLargeFile;

    public boolean isLargeFile() {
        return isLargeFile;
    }

    public void deleteTaskFiles() {
        deleteTempFile();
        deleteTargetFile();
    }

    public void deleteTempFile() {
        final String tempFilePath = getTempFilePath();

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

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

    @Override
    public String toString() {
        return FileDownloadUtils.formatString("id[%d], url[%s], path[%s], status[%d], sofar[%s],"
                        + " total[%d], etag[%s], %s",
                id, url, path, status.get(), soFar, total, eTag,
                super.toString());
    }


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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.id);
        dest.writeString(this.url);
        dest.writeString(this.path);
        dest.writeByte(this.pathAsDirectory ? (byte) 1 : (byte) 0);
        dest.writeString(this.filename);
        dest.writeByte((byte) this.status.get());
        dest.writeLong(this.soFar.get());
        dest.writeLong(this.total);
        dest.writeString(this.errMsg);
        dest.writeString(this.eTag);
        dest.writeInt(this.connectionCount);
        dest.writeByte(this.isLargeFile ? (byte) 1 : (byte) 0);
    }

    public FileDownloadModel() {
        this.soFar = new AtomicLong();
        this.status = new AtomicInteger();
    }

    protected FileDownloadModel(Parcel in) {
        this.id = in.readInt();
        this.url = in.readString();
        this.path = in.readString();
        this.pathAsDirectory = in.readByte() != 0;
        this.filename = in.readString();
        this.status = new AtomicInteger(in.readByte());
        this.soFar = new AtomicLong(in.readLong());
        this.total = in.readLong();
        this.errMsg = in.readString();
        this.eTag = in.readString();
        this.connectionCount = in.readInt();
        this.isLargeFile = in.readByte() != 0;
    }

    public static final Parcelable.Creator<FileDownloadModel> CREATOR =
            new Parcelable.Creator<FileDownloadModel>() {
                @Override
                public FileDownloadModel createFromParcel(Parcel source) {
                    return new FileDownloadModel(source);
                }

                @Override
                public FileDownloadModel[] newArray(int size) {
                    return new FileDownloadModel[size];
                }
            };
} public interface FileDownloadOutputStream {

    /**
     * Writes <code>len</code> bytes from the specified byte array
     * starting at offset <code>off</code> to this file.
     *
     * @param b   the data.
     * @param off the start offset in the data.
     * @param len the number of bytes to write.
     * @throws IOException if an I/O error occurs.
     */
    void write(byte b[], int off, int len) throws IOException;

    /**
     * Flush all buffer to system and force all system buffers to synchronize with the underlying
     * device.
     * <p>
     * This method must ensure all data whatever on buffers of VM or buffers of system for this
     * output stream must persist on the physical media, otherwise the breakpoint will not be
     * integrity.
     *
     * @throws SyncFailedException Thrown when the buffers cannot be flushed,
     *                             or because the system cannot guarantee that all the
     *                             buffers have been synchronized with physical media.
     * @see OutputStream#flush()
     * @see FileDescriptor#sync()
     */
    void flushAndSync() throws IOException;

    /**
     * Closes this output stream and releases any system resources associated with this stream. The
     * general contract of <code>close</code> is that it closes the output stream. A closed stream
     * cannot perform output operations and cannot be reopened.
     * <p>
     * The <code>close</code> method of <code>OutputStream</code> does nothing.
     *
     * @throws IOException if an I/O error occurs.
     */
    void close() throws IOException;

    /**
     * Sets the file-pointer offset, measured from the beginning of this file, at which the next
     * read or write occurs.  The offset may be set beyond the end of the file. Setting the offset
     * beyond the end of the file does not change the file length.  The file length will change only
     * by writing after the offset has been set beyond the end of the file.
     *
     * @param offset the offset position, measured in bytes from the
     *               beginning of the file, at which to set the file
     *               pointer.
     * @throws IOException            if <code>offset</code> is less than
     *                                <code>0</code> or if an I/O error occurs.
     * @throws IllegalAccessException if in this output stream doesn't support this function.
     *                                You can return {@code false} in
     *                                FileDownloadHelper.OutputStreamCreator#supportSeek()
     *                                let the internal mechanism can predict this situation, and
     *                                handle it smoothly.
     * @see java.io.RandomAccessFile#seek(long)
     * @see java.nio.channels.FileChannel#position(long)
     */
    void seek(long offset) throws IOException, IllegalAccessException;

    /**
     * Sets the length of this file.
     * <p>
     * <p> If the present length of the file as returned by the <code>length</code> method is
     * greater than the <code>newLength</code> argument then the file will be truncated.  In this
     * case, if the file offset as returned by the <code>getFilePointer</code> method is greater
     * than <code>newLength</code> then after this method returns the offset will be equal to
     * <code>newLength</code>.
     * <p>
     * <p> If the present length of the file as returned by the <code>length</code> method is
     * smaller than the <code>newLength</code> argument then the file will be extended.  In this
     * case, the contents of the extended portion of the file are not defined.
     *
     * @param newLength The desired length of the file
     * @throws IOException            If an I/O error occurs
     * @throws IllegalAccessException If in this output stream doesn't support this function.
     * @see java.io.RandomAccessFile#setLength(long)
     */
    void setLength(final long newLength) throws IOException, IllegalAccessException;
} public class FileDownloadRandomAccessFile implements FileDownloadOutputStream {
    private final BufferedOutputStream out;
    private final FileDescriptor fd;
    private final RandomAccessFile randomAccess;

    FileDownloadRandomAccessFile(File file) throws IOException {
        randomAccess = new RandomAccessFile(file, "rw");
        fd = randomAccess.getFD();
        out = new BufferedOutputStream(new FileOutputStream(randomAccess.getFD()));
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        out.write(b, off, len);
    }

    @Override
    public void flushAndSync() throws IOException {
        out.flush();
        fd.sync();
    }

    @Override
    public void close() throws IOException {
        out.close();
        randomAccess.close();
    }

    @Override
    public void seek(long offset) throws IOException {
        randomAccess.seek(offset);
    }

    @Override
    public void setLength(long totalBytes) throws IOException {
        randomAccess.setLength(totalBytes);
    }

    public static class Creator implements FileDownloadHelper.OutputStreamCreator {

        @Override
        public FileDownloadOutputStream create(File file) throws IOException {
            return new FileDownloadRandomAccessFile(file);
        }

        @Override
        public boolean supportSeek() {
            return true;
        }
    }
} public class FileDownloadHelper {

    @SuppressLint("StaticFieldLeak")
    private static Context APP_CONTEXT;

    public static void holdContext(final Context context) {
        APP_CONTEXT = context;
    }

    public static Context getAppContext() {
        return APP_CONTEXT;
    }

    @SuppressWarnings("UnusedParameters")
    public interface IdGenerator {
        /**
         * Invoke this method when the data is restoring from the database.
         *
         * @param oldId           the old download id which is generated through the different.
         * @param url             the url path.
         * @param path            the download path.
         * @param pathAsDirectory {@code true}: if the {@code path} is absolute directory to store
         *                        the downloading file, and the {@code filename} will be found in
         *                        contentDisposition from the response as default, if can't find
         *                        contentDisposition,the {@code filename} will be generated by
         *                        {@link FileDownloadUtils#generateFileName(String)}  with
         *                        {@code url}.
         *                        </p>
         *                        {@code false}: if the {@code path} = (absolute directory/filename)
         * @return the new download id.
         */
        int transOldId(final int oldId, final String url, final String path,
                       final boolean pathAsDirectory);

        /**
         * Invoke this method when there is a new task from very beginning.
         * <p>
         * Important Ting: this method would be used on the FileDownloadService and the upper-layer,
         * so as default FileDownloadService is running on the `:filedownloader` process, and
         * upper-layer
         * is on the user process, in this case, if you want to cache something on this instance, it
         * would be two different caches on two processes.
         * <p>
         * Tips: if you want the FileDownloadService runs on the upper-layer process too, just
         * config {@code process.non-separate=true} on the filedownloader.properties, more detail
         * please move to {@link FileDownloadProperties}
         *
         * @param url             the download url.
         * @param path            the download path.
         * @param pathAsDirectory {@code true}: if the {@code path} is absolute directory to store
         *                        the downloading file, and the {@code filename} will be found in
         *                        contentDisposition from the response as default, if can't find
         *                        contentDisposition,the {@code filename} will be generated by
         *                        {@link FileDownloadUtils#generateFileName(String)}  with
         *                        {@code url}.
         *                        </p>
         *                        {@code false}: if the {@code path} = (absolute directory/filename)
         * @return the download task identify.
         */
        int generateId(final String url, final String path, final boolean pathAsDirectory);
    }

    @SuppressWarnings("UnusedParameters")
    public interface ConnectionCountAdapter {
        /**
         * Before invoke this method to determine how many connection will be used to downloading
         * this task,
         * there are several conditions must be confirmed:
         * <p>
         * 1. the connection is support multiple connection(SUPPORT"Partial Content(206)" AND NOT
         * Chunked)
         * 2. the current {@link FileDownloadOutputStream} support seek(The default
         * one({@link FileDownloadRandomAccessFile} is support)
         * 3. this is a new task NOT resume from breakpoint( If the task resume from breakpoint
         * the connection count would be using
         * the one you determined when the task
         * first created ).
         * <p/>
         * The best strategy is refer to how much speed of each connection for the ip:port not file
         * size.
         *
         * @param downloadId  the download id.
         * @param url         the task url.
         * @param path        the task path.
         * @param totalLength the total length of the file.
         * @return the count of connection you want for the task. the value must be large than 0.
         */
        int determineConnectionCount(int downloadId, String url, String path, long totalLength);
    }

    public interface DatabaseCustomMaker {
        /**
         * The database is used for storing the {@link FileDownloadModel}.
         * <p/>
         * The data stored in the database is only used for task resumes from the breakpoint.
         * <p>
         * The task of the data stored in the database must be a task that has not finished
         * downloading yet, and if the task has finished downloading, its data will be
         * {@link FileDownloadDatabase#remove(int)} from the database, since that data is no longer
         * available for resumption of its task pass.
         *
         * @return Nullable, Customize {@link FileDownloadDatabase} which will be used for storing
         * downloading model.
         * @see SqliteDatabaseImpl
         */
        FileDownloadDatabase customMake();
    }

    public interface OutputStreamCreator {
        /**
         * The output stream creator is used for creating {@link FileDownloadOutputStream} which is
         * used to write the input stream to the file for downloading.
         * <p>
         * <strong>Note:</strong> please create a output stream which append the content to the
         * exist file, which means that bytes would be written to the end of the file rather than
         * the beginning.
         *
         * @param file the file will used for storing the downloading content.
         * @return The output stream used to write downloading byte array to the {@code file}.
         * @throws FileNotFoundException if the file exists but is a directory
         *                               rather than a regular file, does not exist but cannot
         *                               be created, or cannot be opened for any other reason
         * @see FileDownloadRandomAccessFile.Creator
         */
        FileDownloadOutputStream create(File file) throws IOException;

        /**
         * @return {@code true} if the {@link FileDownloadOutputStream} is created through
         * {@link #create(File)} support {@link FileDownloadOutputStream#seek(long)} function.
         * If the {@link FileDownloadOutputStream} is created through {@link #create(File)} doesn't
         * support {@link FileDownloadOutputStream#seek(long)}, please return {@code false}, in
         * order to let the internal mechanism can predict this situation, and handle it smoothly.
         */
        boolean supportSeek();
    }

    public interface ConnectionCreator {
        /**
         * The connection creator is used for creating {@link FileDownloadConnection} component
         * which is used to use some protocol to connect to the remote server.
         *
         * @param url the uniform resource locator, which direct the aim resource we need to connect
         * @return The connection creator.
         * @throws IOException if an I/O exception occurs.
         */
        FileDownloadConnection create(String url) throws IOException;
    }

    /**
     * @param id              the {@code id} used for filter out which task would be notified the
     *                        'completed' message if need.
     * @param path            if the file with {@code path} is exist it means the relate task would
     *                        be completed.
     * @param forceReDownload whether the task is force to re-download ignore whether the file has
     *                        been exist or not.
     * @param flowDirectly    {@code true} if flow the message if need directly without throw to the
     *                        message-queue.
     * @return whether the task with {@code id} has been downloaded.
     */
    public static boolean inspectAndInflowDownloaded(int id, String path, boolean forceReDownload,
                                                     boolean flowDirectly) {
        if (forceReDownload) {
            return false;
        }

        if (path != null) {
            final File file = new File(path);
            if (file.exists()) {
                MessageSnapshotFlow.getImpl().inflow(MessageSnapshotTaker.
                        catchCanReusedOldFile(id, file, flowDirectly));
                return true;
            }
        }

        return false;
    }

    /**
     * @param id           the {@code id} used for filter out which task would be notified the
     *                     'warn' message if need.
     * @param model        the target model will be checked for judging whether it is downloading.
     * @param monitor      the monitor for download-thread.
     * @param flowDirectly {@code true} if flow the message if need directly without throw to the
     *                     message-queue.
     * @return whether the {@code model} is downloading.
     */
    public static boolean inspectAndInflowDownloading(int id, FileDownloadModel model,
                                                      IThreadPoolMonitor monitor,
                                                      boolean flowDirectly) {
        if (monitor.isDownloading(model)) {
            MessageSnapshotFlow.getImpl().
                    inflow(MessageSnapshotTaker.catchWarn(id, model.getSoFar(), model.getTotal(),
                            flowDirectly));
            return true;
        }

        return false;
    }

    /**
     * @param id             the {@code id} used for filter out which task would be notified the
     *                       'error' message if need.
     * @param sofar          the so far bytes of the current checking-task.
     * @param tempFilePath   the temp file path(file path used for downloading) for the current
     *                       checking-task.
     * @param targetFilePath the target file path for the current checking-task.
     * @param monitor        the monitor for download-thread.
     * @return whether the task with {@code id} is refused to start, because of there is an another
     * running task with the same {@code tempFilePath}.
     */
    public static boolean inspectAndInflowConflictPath(int id, long sofar,
                                                       String tempFilePath, String targetFilePath,
                                                       IThreadPoolMonitor monitor) {
        if (targetFilePath != null && tempFilePath != null) {
            final int anotherSameTempPathTaskId =
                    monitor.findRunningTaskIdBySameTempPath(tempFilePath, id);
            if (anotherSameTempPathTaskId != 0) {
                MessageSnapshotFlow.getImpl().
                        inflow(MessageSnapshotTaker.catchException(id, sofar,
                                new PathConflictException(anotherSameTempPathTaskId, tempFilePath,
                                        targetFilePath)));
                return true;
            }
        }

        return false;
    }
} public class FileDownloadLog {

    public static boolean NEED_LOG = false;

    private static final String TAG = "FileDownloader.";

    public static void e(Object o, Throwable e, String msg, Object... args) {
        log(Log.ERROR, o, e, msg, args);
    }

    public static void e(Object o, String msg, Object... args) {
        log(Log.ERROR, o, msg, args);
    }

    public static void i(Object o, String msg, Object... args) {
        log(Log.INFO, o, msg, args);
    }

    public static void d(Object o, String msg, Object... args) {
        log(Log.DEBUG, o, msg, args);
    }

    public static void w(Object o, String msg, Object... args) {
        log(Log.WARN, o, msg, args);
    }

    public static void v(Object o, String msg, Object... args) {
        log(Log.VERBOSE, o, msg, args);
    }

    private static void log(int priority, Object o, String message, Object... args) {
        log(priority, o, null, message, args);
    }

    private static void log(int priority, Object o, Throwable throwable, String message,
                            Object... args) {
        final boolean force = priority >= Log.WARN;
        if (!force && !NEED_LOG) {
            return;
        }

        Log.println(priority, getTag(o), FileDownloadUtils.formatString(message, args));
        if (throwable != null) {
            throwable.printStackTrace();
        }
    }

    private static String getTag(final Object o) {
        return TAG + ((o instanceof Class)
                ? ((Class) o).getSimpleName() : o.getClass().getSimpleName());
    }
} public class FileDownloadProperties {

    private static final String KEY_HTTP_LENIENT = "http.lenient";
    private static final String KEY_PROCESS_NON_SEPARATE = "process.non-separate";
    private static final String KEY_DOWNLOAD_MIN_PROGRESS_STEP = "download.min-progress-step";
    private static final String KEY_DOWNLOAD_MIN_PROGRESS_TIME = "download.min-progress-time";
    private static final String KEY_DOWNLOAD_MAX_NETWORK_THREAD_COUNT =
            "download.max-network-thread-count";
    private static final String KEY_FILE_NON_PRE_ALLOCATION = "file.non-pre-allocation";
    private static final String KEY_BROADCAST_COMPLETED = "broadcast.completed";
    private static final String KEY_TRIAL_CONNECTION_HEAD_METHOD
            = "download.trial-connection-head-method";

    public final int downloadMinProgressStep;
    public final long downloadMinProgressTime;
    public final boolean httpLenient;
    public final boolean processNonSeparate;
    public final int downloadMaxNetworkThreadCount;
    public final boolean fileNonPreAllocation;
    public final boolean broadcastCompleted;
    public final boolean trialConnectionHeadMethod;

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

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

    private static final String TRUE_STRING = "true";
    private static final String FALSE_STRING = "false";

    // init properties, normally consume <= 2ms
    private FileDownloadProperties() {
        if (FileDownloadHelper.getAppContext() == null) {
            throw new IllegalStateException("Please invoke the 'FileDownloader#setup' before using "
                    + "FileDownloader. If you want to register some components on FileDownloader "
                    + "please invoke the 'FileDownloader#setupOnApplicationOnCreate' on the "
                    + "'Application#onCreate' first.");
        }

        final long start = System.currentTimeMillis();
        String httpLenient = null;
        String processNonSeparate = null;
        String downloadMinProgressStep = null;
        String downloadMinProgressTime = null;
        String downloadMaxNetworkThreadCount = null;
        String fileNonPreAllocation = null;
        String broadcastCompleted = null;
        String downloadTrialConnectionHeadMethod = null;

        Properties p = new Properties();
        InputStream inputStream = null;

        try {
            inputStream = FileDownloadHelper.getAppContext().getAssets().
                    open("filedownloader.properties");
            if (inputStream != null) {
                p.load(inputStream);
                httpLenient = p.getProperty(KEY_HTTP_LENIENT);
                processNonSeparate = p.getProperty(KEY_PROCESS_NON_SEPARATE);
                downloadMinProgressStep = p.getProperty(KEY_DOWNLOAD_MIN_PROGRESS_STEP);
                downloadMinProgressTime = p.getProperty(KEY_DOWNLOAD_MIN_PROGRESS_TIME);
                downloadMaxNetworkThreadCount = p
                        .getProperty(KEY_DOWNLOAD_MAX_NETWORK_THREAD_COUNT);
                fileNonPreAllocation = p.getProperty(KEY_FILE_NON_PRE_ALLOCATION);
                broadcastCompleted = p.getProperty(KEY_BROADCAST_COMPLETED);
                downloadTrialConnectionHeadMethod = p.getProperty(KEY_TRIAL_CONNECTION_HEAD_METHOD);
            }
        } catch (IOException e) {
            if (e instanceof FileNotFoundException) {
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(FileDownloadProperties.class,
                            "not found filedownloader.properties");
                }
            } else {
                e.printStackTrace();
            }
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


        //http.lenient
        if (httpLenient != null) {
            if (!httpLenient.equals(TRUE_STRING) && !httpLenient.equals(FALSE_STRING)) {
                throw new IllegalStateException(
                        FileDownloadUtils.formatString("the value of '%s' must be '%s' or '%s'",
                                KEY_HTTP_LENIENT, TRUE_STRING, FALSE_STRING));
            }
            this.httpLenient = httpLenient.equals(TRUE_STRING);
        } else {
            this.httpLenient = false;
        }

        //process.non-separate
        if (processNonSeparate != null) {
            if (!processNonSeparate.equals(TRUE_STRING)
                    && !processNonSeparate.equals(FALSE_STRING)) {
                throw new IllegalStateException(
                        FileDownloadUtils.formatString("the value of '%s' must be '%s' or '%s'",
                                KEY_PROCESS_NON_SEPARATE, TRUE_STRING, FALSE_STRING));
            }
            this.processNonSeparate = processNonSeparate.equals(TRUE_STRING);
        } else {
            this.processNonSeparate = false;
        }

        //download.min-progress-step
        if (downloadMinProgressStep != null) {
            int processDownloadMinProgressStep = Integer.valueOf(downloadMinProgressStep);
            processDownloadMinProgressStep = Math.max(0, processDownloadMinProgressStep);
            this.downloadMinProgressStep = processDownloadMinProgressStep;
        } else {
            this.downloadMinProgressStep = 65536;
        }

        //download.min-progress-time
        if (downloadMinProgressTime != null) {
            long processDownloadMinProgressTime = Long.valueOf(downloadMinProgressTime);
            processDownloadMinProgressTime = Math.max(0, processDownloadMinProgressTime);
            this.downloadMinProgressTime = processDownloadMinProgressTime;
        } else {
            this.downloadMinProgressTime = 2000L;
        }

        //download.max-network-thread-count
        if (downloadMaxNetworkThreadCount != null) {
            this.downloadMaxNetworkThreadCount = getValidNetworkThreadCount(
                    Integer.valueOf(downloadMaxNetworkThreadCount));
        } else {
            this.downloadMaxNetworkThreadCount = 3;
        }

        // file.non-pre-allocation
        if (fileNonPreAllocation != null) {
            if (!fileNonPreAllocation.equals(TRUE_STRING)
                    && !fileNonPreAllocation.equals(FALSE_STRING)) {
                throw new IllegalStateException(
                        FileDownloadUtils.formatString("the value of '%s' must be '%s' or '%s'",
                                KEY_FILE_NON_PRE_ALLOCATION, TRUE_STRING, FALSE_STRING));
            }
            this.fileNonPreAllocation = fileNonPreAllocation.equals(TRUE_STRING);
        } else {
            this.fileNonPreAllocation = false;
        }

        // broadcast.completed
        if (broadcastCompleted != null) {
            if (!broadcastCompleted.equals(TRUE_STRING)
                    && !broadcastCompleted.equals(FALSE_STRING)) {
                throw new IllegalStateException(
                        FileDownloadUtils.formatString("the value of '%s' must be '%s' or '%s'",
                                KEY_BROADCAST_COMPLETED, TRUE_STRING, FALSE_STRING));
            }
            this.broadcastCompleted = broadcastCompleted.equals(TRUE_STRING);

        } else {
            this.broadcastCompleted = false;
        }

        // download.trial-connection-head-method
        if (downloadTrialConnectionHeadMethod != null) {
            if (!downloadTrialConnectionHeadMethod.equals(TRUE_STRING)
                    && !downloadTrialConnectionHeadMethod.equals(FALSE_STRING)) {
                throw new IllegalStateException(
                        FileDownloadUtils.formatString("the value of '%s' must be '%s' or '%s'",
                                KEY_TRIAL_CONNECTION_HEAD_METHOD, TRUE_STRING, FALSE_STRING));
            }
            this.trialConnectionHeadMethod = downloadTrialConnectionHeadMethod.equals(TRUE_STRING);
        } else {
            this.trialConnectionHeadMethod = false;
        }

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.i(FileDownloadProperties.class, "init properties %d\n load properties:"
                            + " %s=%B; %s=%B; %s=%d; %s=%d; %s=%d; %s=%B; %s=%B; %s=%B",
                    System.currentTimeMillis() - start,
                    KEY_HTTP_LENIENT, this.httpLenient,
                    KEY_PROCESS_NON_SEPARATE, this.processNonSeparate,
                    KEY_DOWNLOAD_MIN_PROGRESS_STEP, this.downloadMinProgressStep,
                    KEY_DOWNLOAD_MIN_PROGRESS_TIME, this.downloadMinProgressTime,
                    KEY_DOWNLOAD_MAX_NETWORK_THREAD_COUNT, this.downloadMaxNetworkThreadCount,
                    KEY_FILE_NON_PRE_ALLOCATION, this.fileNonPreAllocation,
                    KEY_BROADCAST_COMPLETED, this.broadcastCompleted,
                    KEY_TRIAL_CONNECTION_HEAD_METHOD, this.trialConnectionHeadMethod);
        }
    }

    public static int getValidNetworkThreadCount(int requireCount) {
        int maxValidNetworkThreadCount = 12;
        int minValidNetworkThreadCount = 1;

        if (requireCount > maxValidNetworkThreadCount) {
            FileDownloadLog.w(FileDownloadProperties.class, "require the count of network thread  "
                            + "is %d, what is more than the max valid count(%d), so adjust to %d "
                            + "auto",
                    requireCount, maxValidNetworkThreadCount, maxValidNetworkThreadCount);
            return maxValidNetworkThreadCount;
        } else if (requireCount < minValidNetworkThreadCount) {
            FileDownloadLog.w(FileDownloadProperties.class, "require the count of network thread  "
                            + "is %d, what is less than the min valid count(%d), so adjust to %d"
                            + " auto",
                    requireCount, minValidNetworkThreadCount, minValidNetworkThreadCount);
            return minValidNetworkThreadCount;
        }

        return requireCount;
    }
} @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.
     *                        <p>
     *                        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.
     *                        <p/>
     *                        More smaller more frequently, then download more slowly, but will more
     *                        safer in scene of the process is killed unexpected.
     *                        <p/>
     *                        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.
     *                        <p>
     *                        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.
     *                        <p/>
     *                        More smaller more frequently, then download more slowly, but will more
     *                        safer in scene of the process is killed unexpected.
     *                        <p/>
     *                        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;
        }

        boolean useExternalStorage = false;
        if (FileDownloadHelper.getAppContext().getExternalCacheDir() != null) {
            if (Environment.getExternalStorageState().equals("mounted")) {
                if (Environment.getExternalStorageDirectory().getFreeSpace() > 0) {
                    useExternalStorage = true;
                }
            }
        }

        if (useExternalStorage) {
            return FileDownloadHelper.getAppContext().getExternalCacheDir().getAbsolutePath();
        } else {
            return FileDownloadHelper.getAppContext().getCacheDir().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.
     * <p>
     * If {@code true}, You can check whether the file has completed downloading with
     * {@link File#exists()} directly.
     * <p>
     * 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.
     * </p>
     * 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 = findFileNameFromUrl(url);
        }

        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);
    }

    static String findFileNameFromUrl(String url) {
        if (url == null || url.isEmpty()) {
            return null;
        }
        try {
            final URL parseUrl = new URL(url);
            final String path = parseUrl.getPath();
            String fileName = path.substring(path.lastIndexOf('/') + 1);
            if (fileName.isEmpty()) return null;
            return fileName;
        } catch (MalformedURLException ignore) {
        }
        return null;
    }
}
import com.liulishuo.filedownloader.connection.DefaultConnectionCountAdapter;
import com.liulishuo.filedownloader.connection.FileDownloadUrlConnection;
import com.liulishuo.filedownloader.database.FileDownloadDatabase;
import com.liulishuo.filedownloader.database.RemitDatabase;
import com.liulishuo.filedownloader.model.FileDownloadModel;
import com.liulishuo.filedownloader.stream.FileDownloadOutputStream;
import com.liulishuo.filedownloader.stream.FileDownloadRandomAccessFile;
import com.liulishuo.filedownloader.util.FileDownloadHelper;
import com.liulishuo.filedownloader.util.FileDownloadLog;
import com.liulishuo.filedownloader.util.FileDownloadProperties;
import com.liulishuo.filedownloader.util.FileDownloadUtils;
/*
 * 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;


/**
 * Params in this class is used in the downloading manager, and would be used for initialize the
 * download manager in the process the downloader service settled on.
 */
public class DownloadMgrInitialParams {

    private final InitCustomMaker mMaker;

    public DownloadMgrInitialParams() {
        mMaker = null;
    }

    public DownloadMgrInitialParams(InitCustomMaker maker) {
        this.mMaker = maker;
    }

    public int getMaxNetworkThreadCount() {
        if (mMaker == null) {
            return getDefaultMaxNetworkThreadCount();
        }

        final Integer customizeMaxNetworkThreadCount = mMaker.mMaxNetworkThreadCount;

        if (customizeMaxNetworkThreadCount != null) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "initial FileDownloader manager with the customize "
                        + "maxNetworkThreadCount: %d", customizeMaxNetworkThreadCount);
            }

            return FileDownloadProperties