public final class DefaultDataSourceFactory {

    public static UrlDataSource createUrlDataSource(String url) {
        return new DefaultUrlDataSource(url);
    }

    public static UrlDataSource createUrlDataSource(DefaultUrlDataSource dataSource) {
        return new DefaultUrlDataSource(dataSource);
    }

    public static FileDataSource createFileDataSource(File file, DiskLruCache diskLruStorage) {
        return new RandomAcessFileDataSource(file, diskLruStorage);
    }

    public static DiskLruCache createDiskLruCache(MediaLoaderConfig mediaLoaderConfig) {
        return new SimpleDiskLruCache(mediaLoaderConfig);
    }
}
 public interface FileDataSource extends Closeable {

    long length() throws IOException;

    int seekAndRead(long offset, byte[] buffer) throws IOException;

    void append(byte[] data, int length) throws IOException;

    void complete() throws IOException;

    boolean isCompleted();

    File getFile();
} public class DefaultUrlDataSource extends BaseUrlDataSource {

    private UrlDataSourceInfo mUrlDataSourceInfo;

    private HttpURLConnection mUrlConnection;

    private InputStream mInputStream;

    public DefaultUrlDataSource(String url) {
        UrlDataSourceInfo sourceInfo = mUrlDataSourceInfoCache.get(url);
        if (sourceInfo != null) {
            mUrlDataSourceInfo = sourceInfo;
        } else {
            mUrlDataSourceInfo = new UrlDataSourceInfo(url, Integer.MIN_VALUE, Util.getMimeTypeFromUrl(url));
        }
    }

    public DefaultUrlDataSource(DefaultUrlDataSource source) {
        this.mUrlDataSourceInfo = source.mUrlDataSourceInfo;
        this.mUrlDataSourceInfoCache = source.mUrlDataSourceInfoCache;
    }

    @Override
    public void open(long offset) throws IOException {
        try {
            LogUtil.d("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + mUrlDataSourceInfo.url);
            mUrlConnection = (HttpURLConnection) new URL(mUrlDataSourceInfo.url).openConnection(Proxy.NO_PROXY);
            if (offset > 0) {
                mUrlConnection.setRequestProperty(HttpHeaders.Names.RANGE, "bytes=" + offset + "-");
            }
            String mime = mUrlConnection.getContentType();
            mInputStream = new BufferedInputStream(mUrlConnection.getInputStream(), Util.DEFAULT_BUFFER_SIZE);
            long length = readSourceAvailableBytes(mUrlConnection, offset, mUrlConnection.getResponseCode());
            this.mUrlDataSourceInfo = new UrlDataSourceInfo(mUrlDataSourceInfo.url, length, mime);
            this.mUrlDataSourceInfoCache.put(mUrlDataSourceInfo.url, mUrlDataSourceInfo);
        } catch (Exception e) {
            throw new IOException("Error opening connection for " + mUrlDataSourceInfo.url + " with offset " + offset, e);
        }
    }

    private long readSourceAvailableBytes(HttpURLConnection connection, long offset, int responseCode) throws IOException {
        int contentLength = connection.getContentLength();
        return responseCode == HTTP_OK ? contentLength
                : responseCode == HTTP_PARTIAL ? contentLength + offset : mUrlDataSourceInfo.length;
    }

    @Override
    public synchronized long length() throws IOException {
        if (mUrlDataSourceInfo.length == Integer.MIN_VALUE) {
            requestUrlDataSourceInfo();
        }
        return mUrlDataSourceInfo.length;
    }

    @Override
    public int read(byte[] buffer) throws IOException {
        try {
            return mInputStream.read(buffer, 0, buffer.length);
        } catch (IOException e) {
            throw new IOException("Error reading data from " + mUrlDataSourceInfo.url, e);
        }
    }

    @Override
    public synchronized String mimeType() throws IOException {
        if (TextUtils.isEmpty(mUrlDataSourceInfo.mimeType)) {
            requestUrlDataSourceInfo();
        }
        return mUrlDataSourceInfo.mimeType;
    }

    @Override
    public void close() throws IOException {
        if (mInputStream != null) {
            mInputStream.close();
        }
        if (mUrlConnection != null) {
            mUrlConnection.disconnect();
        }
        mUrlDataSourceInfoCache.remove(mUrlDataSourceInfo.url);
    }

    private void requestUrlDataSourceInfo() throws IOException {
        HttpURLConnection urlConnection = null;
        try {
            urlConnection = (HttpURLConnection) new URL(mUrlDataSourceInfo.url).openConnection();
            urlConnection.setRequestMethod("HEAD");
            urlConnection.setConnectTimeout(10000);
            urlConnection.setReadTimeout(10000);
            int length = urlConnection.getContentLength();
            String mime = urlConnection.getContentType();
            this.mUrlDataSourceInfo = new UrlDataSourceInfo(mUrlDataSourceInfo.url, length, mime);
            this.mUrlDataSourceInfoCache.put(mUrlDataSourceInfo.url, mUrlDataSourceInfo);
            LogUtil.d("requestUrlDataSourceInfo: " + mUrlDataSourceInfo.toString());
        } catch (IOException e) {
            LogUtil.e("Error request addHeader info from " + mUrlDataSourceInfo.url, e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
    }

    @Override
    public String getUrl() {
        return mUrlDataSourceInfo.url;
    }
}
 public interface UrlDataSource extends Closeable {

    void open(long offset) throws IOException;

    long length() throws IOException;

    int read(byte[] buffer) throws IOException;

    String mimeType() throws IOException;

    String getUrl();
} public interface DownloadListener {

    void onProgress(String url, File file, int progress);

    void onError(Throwable e);
} public class DownloadTask implements Runnable {

    private UrlDataSource mUrlDataSource;

    private FileDataSource mFileDataSource;

    private final Object mStopLock = new Object();
    private volatile boolean stopped = false;

    private final Object mPauseLock = new Object();
    private final AtomicBoolean mPaused = new AtomicBoolean(false);

    private volatile int mDownloadPercent;

    private DownloadListener mDownloadListener;

    public DownloadTask(DownloadManager.Request request, MediaLoaderConfig mediaLoaderConfig, DownloadListener listener) {
        mUrlDataSource = DefaultDataSourceFactory.createUrlDataSource(request.getUrl());
        mFileDataSource = DefaultDataSourceFactory.createFileDataSource(new File(mediaLoaderConfig.cacheRootDir, mediaLoaderConfig.cacheFileNameGenerator.create(request.getUrl())), mediaLoaderConfig.diskLruCache);
        mDownloadListener = listener;
    }

    public DownloadTask(UrlDataSource urlDataSource, FileDataSource fileDataSource, DownloadListener listener) {
        mUrlDataSource = urlDataSource;
        mFileDataSource = fileDataSource;
        mDownloadListener = listener;
    }

    @Override
    public void run() {
//        android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);//避免和主线程抢占资源发生ANR
        long urlDataSourceLength;
        long offset;
        try {
            offset = mFileDataSource.length();
            mUrlDataSource.open(offset);
            urlDataSourceLength = mUrlDataSource.length();
            byte[] buffer = new byte[Util.DEFAULT_BUFFER_SIZE];
            int readBytes;
            while ((readBytes = mUrlDataSource.read(buffer)) != -1) {
                waitIfPaused();
                synchronized (mStopLock) {
                    if (isStopped()) {
                        return;
                    }
                    mFileDataSource.append(buffer, readBytes);
                }
                offset += readBytes;
                notifyNewDataAvailable(offset, urlDataSourceLength);
            }
            tryComplete();
        } catch (Throwable e) {
            if (mDownloadListener != null) {
                mDownloadListener.onError(e);
            }
            LogUtil.e(e);
        } finally {
            try {
                mUrlDataSource.close();
            } catch (IOException e) {
                LogUtil.e("error close url data source ", e);
            }
        }
    }

    private void onDownloadPercentUpdated(int downloadProgress) {
        if (mDownloadListener != null) {
            mDownloadListener.onProgress(mUrlDataSource.getUrl(), mFileDataSource.getFile(), downloadProgress);
//            LogUtil.e("Url " + mUrlDataSource.getUrl() + " download progress:" + downloadProgress);
        }
    }

    private void notifyNewDataAvailable(long fileDataSourceAvailable, long urlDataSourceLength) {
        int newPercent = (urlDataSourceLength == 0) ? 100 : (int) (fileDataSourceAvailable * 100 / urlDataSourceLength);
        if (newPercent > mDownloadPercent + 2) {//防止通知太快导致ui掉帧
            onDownloadPercentUpdated(newPercent);
            mDownloadPercent = newPercent;
        }
    }

    private void tryComplete() throws IOException {
        mDownloadPercent = 100;
        onDownloadPercentUpdated(mDownloadPercent);
        synchronized (mStopLock) {
            if (!isStopped() && mFileDataSource.length() == mUrlDataSource.length()) {
                mFileDataSource.complete();
            }
        }
    }

    public boolean isStopped() {
        return Thread.currentThread().isInterrupted() || stopped;
    }

    private void waitIfPaused() {
        if (mPaused.get()) {
            synchronized (mPauseLock) {
                try {
                    mPauseLock.wait();
                } catch (InterruptedException e) {

                }
            }
        }
    }

    public void pause() {
        mPaused.set(true);
    }

    public void resume() {
        mPaused.set(false);
        synchronized (mPauseLock) {
            mPauseLock.notifyAll();
        }
    }

    public Thread getCurrentThread() {
        return Thread.currentThread();
    }

    public void stop() {
        synchronized (mStopLock) {
            stopped = true;
            try {
                mFileDataSource.close();
            } catch (IOException e) {
                LogUtil.e("error close file dataSource", e);
            }
        }
    }
}
 public class HttpHeaders extends LinkedHashMap<String, String> {

    private static final Pattern HEADER_RANGE_PATTERN = Pattern.compile("bytes=(\\d*)-");

    public interface Names {
        String RANGE = "Range";
        String ACCEPT_RANGES = "Accept-Ranges";
        String CONTENT_LENGTH = "Content-Length";
        String CONTENT_RANGE = "Content-Range";
        String CONTENT_TYPE = "Content-Type";
    }

    public interface Values {
        String BYTES = "bytes";
    }

    private long mRangeOffset = Long.MIN_VALUE;

    public long getRangeOffset() {
        if (mRangeOffset == Long.MIN_VALUE) {
            String range = get(HttpHeaders.Names.RANGE);
            if (!TextUtils.isEmpty(range)) {
                Matcher matcher = HEADER_RANGE_PATTERN.matcher(range);
                if (matcher.find()) {
                    String rangeValue = matcher.group(1);
                    mRangeOffset = Long.parseLong(rangeValue);
                }
            }
        }
        return Math.max(mRangeOffset, 0);
    }

    public boolean isPartial() {
        return containsKey(HttpHeaders.Names.RANGE);
    }
}
 public class HttpResponseEncoder implements ResponseEncoder<HttpResponse> {

    @Override
    public byte[] encode(HttpResponse response) throws IOException {
        StringBuilder sb = new StringBuilder();
        sb.append(response.protocol().toString()).append(HttpConstants.SP).append(response.status().toString());
        sb.append(HttpConstants.CRLF);//status line end
        for (Map.Entry<String, String> entry : response.headers().entrySet()) {
            sb.append(entry.getKey()).append(HttpConstants.COLON).append(entry.getValue()).append(HttpConstants.CRLF);
        }
        sb.append(HttpConstants.CRLF);//headers end
        return sb.toString().getBytes();
    }
}
 public interface ResponseEncoder<T extends Response> {

    byte[] encode(T response) throws IOException;
}
 public interface Request {

    HttpMethod method();

    String url();

    HttpVersion protocol();

    HttpHeaders headers();

    String getParam(String name);
}
 public interface Response {

    HttpStatus status();

    void setStatus(HttpStatus status);

    HttpVersion protocol();

    HttpHeaders headers();

    /**
     * 添加头部
     *
     * @param key   {@link com.vincan.medialoader.tinyhttpd.HttpHeaders.Names}
     * @param value
     */
    void addHeader(String key, String value);

    void write(byte[] bytes) throws IOException;

    void write(byte[] bytes, int offset, int length) throws IOException;
}
 public final class ResponseException extends Exception {

    private final HttpStatus status;

    public ResponseException(HttpStatus status) {
        super(status.desc);
        this.status = status;
    }

    public ResponseException(HttpStatus status, String message) {
        super(message);
        this.status = status;
    }

    public ResponseException(HttpStatus status, String message, Exception e) {
        super(message, e);
        this.status = status;
    }

    public HttpStatus getStatus() {
        return this.status;
    }
} public enum HttpStatus {
    OK(200, "OK"),
    PARTIAL_CONTENT(206, "Partial Content"),

    BAD_REQUEST(400, "Bad Request"),
    UNAUTHORIZED(401, "Unauthorized"),
    FORBIDDEN(403, "Forbidden"),
    NOT_FOUND(404, "Not Found"),
    TOO_MANY_REQUESTS(429, "Too Many Requests"),

    INTERNAL_ERROR(500, "Internal Server Error");

    public final int code;

    public final String desc;

    HttpStatus(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    @Override
    public String toString() {
        return code + " " + desc;
    }
} public final class Util {

    public static final String LOCALHOST = "127.0.0.1";//不建议使用localhost，因为需要访问网络

    public static final int DEFAULT_BUFFER_SIZE = 8192;//8 * 1024

    public static final String CHARSET_DEFAULT = "UTF-8";

    private static final int MAX_EXTENSION_LENGTH = 4;

    private Util() {

    }

    public static <T> T notEmpty(T object) {
        if (object instanceof String) {
            if (TextUtils.isEmpty((String) object)) {
                throw new NullPointerException(object.getClass().getSimpleName() + " cann't be empty");
            }
        } else {
            if (object == null) {
                throw new NullPointerException(object.getClass().getSimpleName() + " cann't be null");
            }
        }
        return object;
    }

    public static String encode(String url) {
        try {
            return URLEncoder.encode(url, CHARSET_DEFAULT);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Encoding not supported", e);
        }
    }

    public static String decode(String url) {
        try {
            return URLDecoder.decode(url, CHARSET_DEFAULT);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Decoding not supported", e);
        }
    }

    public static String createUrl(String host, int port, String path) {
        return String.format(Locale.US, "http://%s:%d/%s", host, port, Util.encode(path));
    }

    public static String createUrl(String host, int port, String path, String secret) {
        String timestamp = String.valueOf(System.currentTimeMillis());
        String sign = getHmacSha1(path + timestamp, secret);
        StringBuilder sb = new StringBuilder("http://%s:%d/%s");
        sb.append("?").append(HttpConstants.PARAM_SIGN).append("=%s");
        sb.append("&").append(HttpConstants.PARAM_TIMESTAMP).append("=%s");
        return String.format(Locale.US, sb.toString(), host, port, Util.encode(path), Util.encode(sign), Util.encode(timestamp));
    }

    public static String getHmacSha1(String s, String keyString) {
        String hmacSha1 = null;
        try {
            SecretKeySpec key = new SecretKeySpec((keyString).getBytes(), "HmacSHA1");
            Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(key);
            byte[] bytes = mac.doFinal(s.getBytes());
            hmacSha1 = new String(Base64.encodeToString(bytes, Base64.DEFAULT));
        } catch (InvalidKeyException | NoSuchAlgorithmException e) {

        }
        return hmacSha1;
    }

    public static String getMimeTypeFromUrl(String url) {
        String extension = MimeTypeMap.getFileExtensionFromUrl(url);
        return TextUtils.isEmpty(extension) ? "" : MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    }

    public static String getExtensionFromUrl(String url) {
        int dotIndex = url.lastIndexOf('.');
        int slashIndex = url.lastIndexOf('/');
        return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ?
                url.substring(dotIndex + 1, url.length()) : "";
    }

    public static String getMD5(String string) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] digestBytes = messageDigest.digest(string.getBytes());
            return bytesToHexString(digestBytes);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }

    private static String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            sb.append(String.format("%02x", bytes[i]));// 以十六进制(x)输出,2为指定的输出字段的宽度.如果位数小于2,则左端补0
        }
        return sb.toString();
    }
}

import android.text.TextUtils;
import com.vincan.medialoader.data.DefaultDataSourceFactory;
import com.vincan.medialoader.data.file.FileDataSource;
import com.vincan.medialoader.data.url.DefaultUrlDataSource;
import com.vincan.medialoader.data.url.UrlDataSource;
import com.vincan.medialoader.download.DownloadListener;
import com.vincan.medialoader.download.DownloadTask;
import com.vincan.medialoader.tinyhttpd.HttpHeaders;
import com.vincan.medialoader.tinyhttpd.codec.HttpResponseEncoder;
import com.vincan.medialoader.tinyhttpd.codec.ResponseEncoder;
import com.vincan.medialoader.tinyhttpd.request.Request;
import com.vincan.medialoader.tinyhttpd.response.Response;
import com.vincan.medialoader.tinyhttpd.response.ResponseException;
import com.vincan.medialoader.tinyhttpd.response.HttpStatus;
import com.vincan.medialoader.utils.Util;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutorService;

package com.vincan.medialoader.manager;




/**
 * Media业务实现
 *
 * @author vincanyang
 */
public class MediaManagerImpl implements MediaManager {

    private static final float NO_CACHE_BARRIER = .2f;

    private UrlDataSource mUrlDataSource;

    private FileDataSource mFileDataSource;

    private DownloadListener mDownloadListener;

    private final Object mWaitForDownloadLock = new Object();

    private volatile Thread mDownloadThread;

    private final DownloadTask mDownloadTask;

    private final ExecutorService mDownloadExecutorService;

    private ResponseEncoder mResponseEncoder = new HttpResponseEncoder();

    public MediaManagerImpl(UrlDataSource urlDataSource, FileDataSource fileDataSource, DownloadListener downloadListener, ExecutorService downloadExecutorService) {
        mUrlDataSource = urlDataSource;
        mFileDataSource = fileDataSource;
        mDownloadListener = downloadListener;
        mDownloadTask = new DownloadTask(mUrlDataSource, mFileDataSource, new DownloadListenerImpl());
        mDownloadExecutorService = downloadExecutorService;
    }

    @Override
    public void responseByRequest(Request request, Response response) throws ResponseException, IOException {
        addResponseHeaders(request, response);
        byte[] headersBytes = mResponseEncoder.encode(response);
        response.write(headersBytes);

        long rangeOffset = request.headers().getRangeOffset();
        if (isCacheDataEnough(request)) {
            responseWithCache(rangeOffset, response);
        } else {
            responseWithUrl(rangeOffset, response);
        }
    }

    private void addResponseHeaders(Request request, Response response) throws IOException {
        response.setStatus(request.headers().isPartial() ? HttpStatus.PARTIAL_CONTENT : HttpStatus.OK);

        response.addHeader(HttpHeaders.Names.ACCEPT_RANGES, HttpHeaders.Values.BYTES);
