Pārlūkot izejas kodu

| 针对Nginx服务器的默认要求path的末尾反斜杠会保留

drake 5 gadi atpakaļ
vecāks
revīzija
a33a41b50f
100 mainītis faili ar 9937 papildinājumiem un 1 dzēšanām
  1. 1 1
      README.md
  2. 1 0
      kalle/.gitignore
  3. 11 0
      kalle/build.gradle
  4. 26 0
      kalle/src/main/AndroidManifest.xml
  5. 70 0
      kalle/src/main/java/com/yanzhenjie/kalle/BaseContent.java
  6. 28 0
      kalle/src/main/java/com/yanzhenjie/kalle/Binary.java
  7. 349 0
      kalle/src/main/java/com/yanzhenjie/kalle/BodyRequest.java
  8. 65 0
      kalle/src/main/java/com/yanzhenjie/kalle/CancelerManager.java
  9. 31 0
      kalle/src/main/java/com/yanzhenjie/kalle/Canceller.java
  10. 40 0
      kalle/src/main/java/com/yanzhenjie/kalle/Content.java
  11. 70 0
      kalle/src/main/java/com/yanzhenjie/kalle/FileBinary.java
  12. 61 0
      kalle/src/main/java/com/yanzhenjie/kalle/FileBody.java
  13. 296 0
      kalle/src/main/java/com/yanzhenjie/kalle/FormBody.java
  14. 515 0
      kalle/src/main/java/com/yanzhenjie/kalle/Headers.java
  15. 34 0
      kalle/src/main/java/com/yanzhenjie/kalle/JsonBody.java
  16. 193 0
      kalle/src/main/java/com/yanzhenjie/kalle/Kalle.java
  17. 354 0
      kalle/src/main/java/com/yanzhenjie/kalle/KalleConfig.java
  18. 324 0
      kalle/src/main/java/com/yanzhenjie/kalle/Params.java
  19. 26 0
      kalle/src/main/java/com/yanzhenjie/kalle/ProgressBar.java
  20. 319 0
      kalle/src/main/java/com/yanzhenjie/kalle/Request.java
  21. 22 0
      kalle/src/main/java/com/yanzhenjie/kalle/RequestBody.java
  22. 102 0
      kalle/src/main/java/com/yanzhenjie/kalle/RequestMethod.java
  23. 114 0
      kalle/src/main/java/com/yanzhenjie/kalle/Response.java
  24. 41 0
      kalle/src/main/java/com/yanzhenjie/kalle/ResponseBody.java
  25. 75 0
      kalle/src/main/java/com/yanzhenjie/kalle/StringBody.java
  26. 388 0
      kalle/src/main/java/com/yanzhenjie/kalle/Url.java
  27. 214 0
      kalle/src/main/java/com/yanzhenjie/kalle/UrlBody.java
  28. 208 0
      kalle/src/main/java/com/yanzhenjie/kalle/UrlRequest.java
  29. 34 0
      kalle/src/main/java/com/yanzhenjie/kalle/XmlBody.java
  30. 73 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/BroadcastNetwork.java
  31. 31 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/ConnectFactory.java
  32. 55 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/Connection.java
  33. 41 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/Interceptor.java
  34. 37 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/Network.java
  35. 186 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/NetworkChecker.java
  36. 37 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/RealTimeNetwork.java
  37. 60 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/StreamBody.java
  38. 63 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/http/AppChain.java
  39. 110 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/http/Call.java
  40. 57 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/http/Chain.java
  41. 174 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/http/ConnectInterceptor.java
  42. 87 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/http/LoggerInterceptor.java
  43. 68 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/http/RedirectInterceptor.java
  44. 46 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/http/RetryInterceptor.java
  45. 76 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/stream/NullStream.java
  46. 82 0
      kalle/src/main/java/com/yanzhenjie/kalle/connect/stream/SourceStream.java
  47. 208 0
      kalle/src/main/java/com/yanzhenjie/kalle/cookie/Cookie.java
  48. 168 0
      kalle/src/main/java/com/yanzhenjie/kalle/cookie/CookieManager.java
  49. 74 0
      kalle/src/main/java/com/yanzhenjie/kalle/cookie/CookieStore.java
  50. 195 0
      kalle/src/main/java/com/yanzhenjie/kalle/cookie/DBCookieStore.java
  51. 207 0
      kalle/src/main/java/com/yanzhenjie/kalle/cookie/db/CookieDao.java
  52. 37 0
      kalle/src/main/java/com/yanzhenjie/kalle/cookie/db/Field.java
  53. 67 0
      kalle/src/main/java/com/yanzhenjie/kalle/cookie/db/SQLHelper.java
  54. 146 0
      kalle/src/main/java/com/yanzhenjie/kalle/cookie/db/Where.java
  55. 234 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/BasicWorker.java
  56. 104 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/BodyDownload.java
  57. 46 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/BodyWorker.java
  58. 46 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/Callback.java
  59. 114 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/Download.java
  60. 182 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/DownloadManager.java
  61. 41 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/SimpleCallback.java
  62. 104 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/UrlDownload.java
  63. 46 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/UrlWorker.java
  64. 74 0
      kalle/src/main/java/com/yanzhenjie/kalle/download/Work.java
  65. 36 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/ConnectException.java
  66. 29 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/ConnectTimeoutError.java
  67. 47 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/DownloadError.java
  68. 29 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/HostError.java
  69. 29 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/NetworkError.java
  70. 29 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/NoCacheError.java
  71. 29 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/ParseError.java
  72. 36 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/ReadException.java
  73. 29 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/ReadTimeoutError.java
  74. 29 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/URLError.java
  75. 35 0
      kalle/src/main/java/com/yanzhenjie/kalle/exception/WriteException.java
  76. 67 0
      kalle/src/main/java/com/yanzhenjie/kalle/secure/AESSecret.java
  77. 96 0
      kalle/src/main/java/com/yanzhenjie/kalle/secure/Encryption.java
  78. 43 0
      kalle/src/main/java/com/yanzhenjie/kalle/secure/SafeSecret.java
  79. 32 0
      kalle/src/main/java/com/yanzhenjie/kalle/secure/Secret.java
  80. 296 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/BasicWorker.java
  81. 47 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/BodyWorker.java
  82. 61 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/ByteArrayBody.java
  83. 69 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/Callback.java
  84. 59 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/Converter.java
  85. 205 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/RequestManager.java
  86. 100 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleBodyRequest.java
  87. 55 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleCallback.java
  88. 50 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleRequest.java
  89. 127 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleResponse.java
  90. 100 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleUrlRequest.java
  91. 47 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/UrlWorker.java
  92. 74 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/Work.java
  93. 78 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/cache/Cache.java
  94. 58 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/cache/CacheMode.java
  95. 79 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/cache/CacheStore.java
  96. 148 0
      kalle/src/main/java/com/yanzhenjie/kalle/simple/cache/DiskCacheStore.java
  97. 26 0
      kalle/src/main/java/com/yanzhenjie/kalle/ssl/CompatSSLSocketFactory.java
  98. 34 0
      kalle/src/main/java/com/yanzhenjie/kalle/ssl/SSLUtils.java
  99. 142 0
      kalle/src/main/java/com/yanzhenjie/kalle/ssl/TLSSocketFactory.java
  100. 99 0
      kalle/src/main/java/com/yanzhenjie/kalle/urlconnect/URLConnection.java

+ 1 - 1
README.md

@@ -79,7 +79,7 @@ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
 // 支持自动下拉刷新和缺省页的, 可选
 implementation 'com.github.liangjingkanji:BRV:1.2.1'
 
-implementation 'com.github.liangjingkanji:Net:2.0.4'
+implementation 'com.github.liangjingkanji:Net:2.0.5'
 ```
 
 

+ 1 - 0
kalle/.gitignore

@@ -0,0 +1 @@
+/build

+ 11 - 0
kalle/build.gradle

@@ -0,0 +1,11 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+    buildToolsVersion "28.0.3"
+
+    defaultConfig {
+        minSdkVersion 9
+        targetSdkVersion 28
+    }
+}

+ 26 - 0
kalle/src/main/AndroidManifest.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright © 2018 Zhenjie Yan.
+
+    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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.yanzhenjie.kalle">
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+
+</manifest>

+ 70 - 0
kalle/src/main/java/com/yanzhenjie/kalle/BaseContent.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import com.yanzhenjie.kalle.util.ProgressOutputStream;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.Executor;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public abstract class BaseContent<T extends Content> implements Content {
+
+    private ProgressBar<T> mProgressBar;
+
+    public void onProgress(ProgressBar<T> progressBar) {
+        this.mProgressBar = new AsyncProgressBar<>(progressBar);
+    }
+
+    @Override
+    public final void writeTo(OutputStream writer) throws IOException {
+        if (mProgressBar != null) {
+            onWrite(new ProgressOutputStream<>(writer, (T) this, mProgressBar));
+        } else {
+            onWrite(writer);
+        }
+    }
+
+    /**
+     * Content body data.
+     */
+    protected abstract void onWrite(OutputStream writer) throws IOException;
+
+    private static class AsyncProgressBar<T extends Content> implements ProgressBar<T> {
+
+        private final ProgressBar<T> mProgressBar;
+        private final Executor mExecutor;
+
+        public AsyncProgressBar(ProgressBar<T> bar) {
+            this.mProgressBar = bar;
+            this.mExecutor = Kalle.getConfig().getMainExecutor();
+        }
+
+        @Override
+        public void progress(final T origin, final int progress) {
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mProgressBar.progress(origin, progress);
+                }
+            });
+        }
+    }
+
+}

+ 28 - 0
kalle/src/main/java/com/yanzhenjie/kalle/Binary.java

@@ -0,0 +1,28 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+/**
+ * <p>File interface. All the methods are called in thread.</p>
+ * Created in Oct 12, 2015 4:44:07 PM.
+ */
+public interface Binary extends Content {
+
+    /**
+     * Gets the name of Binary.
+     */
+    String name();
+}

+ 349 - 0
kalle/src/main/java/com/yanzhenjie/kalle/BodyRequest.java

@@ -0,0 +1,349 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public class BodyRequest extends Request {
+
+    private final Url mUrl;
+    private final RequestBody mBody;
+    private final Params mParams;
+
+    protected BodyRequest(Api api) {
+        super(api);
+        this.mUrl = api.mUrlBuilder.build();
+        this.mParams = api.mParams.build();
+        this.mBody = api.mBody == null ? (mParams.hasBinary() ? mParams.toFormBody() : mParams.toUrlBody()) : api.mBody;
+    }
+
+    public static BodyRequest.Builder newBuilder(String url, RequestMethod method) {
+        return newBuilder(Url.newBuilder(url).build(), method);
+    }
+
+    public static BodyRequest.Builder newBuilder(Url url, RequestMethod method) {
+        return new BodyRequest.Builder(url, method);
+    }
+
+    /**
+     * @deprecated use {@link #newBuilder(Url, RequestMethod)} instead.
+     */
+    @Deprecated
+    public static BodyRequest.Builder newBuilder(Url.Builder url, RequestMethod method) {
+        return newBuilder(url.build(), method);
+    }
+
+    @Override
+    public Url url() {
+        return mUrl;
+    }
+
+    @Override
+    public Params copyParams() {
+        return mParams;
+    }
+
+    /**
+     * Get the define body to send.
+     */
+    @Override
+    public RequestBody body() {
+        return mBody;
+    }
+
+    public static class Api<T extends Api<T>> extends Request.Api<T> {
+
+        private Url.Builder mUrlBuilder;
+
+        private Params.Builder mParams;
+        private RequestBody mBody;
+
+        protected Api(Url url, RequestMethod method) {
+            super(method);
+            this.mUrlBuilder = url.builder();
+            this.mParams = Params.newBuilder();
+
+            this.mParams.add(Kalle.getConfig().getParams());
+        }
+
+        @Override
+        public T path(int value) {
+            mUrlBuilder.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(long value) {
+            mUrlBuilder.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(boolean value) {
+            mUrlBuilder.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(char value) {
+            mUrlBuilder.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(double value) {
+            mUrlBuilder.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(float value) {
+            mUrlBuilder.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(String value) {
+            mUrlBuilder.addPath(value);
+            return (T) this;
+        }
+
+        /**
+         * Add parameter to url.
+         */
+        public T urlParam(String key, int value) {
+            mUrlBuilder.addQuery(key, value);
+            return (T) this;
+        }
+
+        /**
+         * Add parameter to url.
+         */
+        public T urlParam(String key, long value) {
+            mUrlBuilder.addQuery(key, value);
+            return (T) this;
+        }
+
+        /**
+         * Add parameter to url.
+         */
+        public T urlParam(String key, boolean value) {
+            mUrlBuilder.addQuery(key, value);
+            return (T) this;
+        }
+
+        /**
+         * Add parameter to url.
+         */
+        public T urlParam(String key, char value) {
+            mUrlBuilder.addQuery(key, value);
+            return (T) this;
+        }
+
+        /**
+         * Add parameter to url.
+         */
+        public T urlParam(String key, double value) {
+            mUrlBuilder.addQuery(key, value);
+            return (T) this;
+        }
+
+        /**
+         * Add parameter to url.
+         */
+        public T urlParam(String key, float value) {
+            mUrlBuilder.addQuery(key, value);
+            return (T) this;
+        }
+
+        /**
+         * Add parameter to url.
+         */
+        public T urlParam(String key, short value) {
+            mUrlBuilder.addQuery(key, value);
+            return (T) this;
+        }
+
+        /**
+         * Add parameter to url.
+         */
+        public T urlParam(String key, String value) {
+            mUrlBuilder.addQuery(key, value);
+            return (T) this;
+        }
+
+        /**
+         * Add parameter to url.
+         */
+        public T urlParam(String key, List<String> values) {
+            mUrlBuilder.addQuery(key, values);
+            return (T) this;
+        }
+
+        /**
+         * Add parameters to url.
+         */
+        public T urlParam(Params params) {
+            mUrlBuilder.addQuery(params);
+            return (T) this;
+        }
+
+        /**
+         * Set parameter to url.
+         */
+        public T setUrlParam(Params params) {
+            mUrlBuilder.setQuery(params);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, int value) {
+            return param(key, Integer.toString(value));
+        }
+
+        @Override
+        public T param(String key, long value) {
+            mParams.add(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, boolean value) {
+            mParams.add(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, char value) {
+            mParams.add(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, double value) {
+            mParams.add(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, float value) {
+            mParams.add(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, short value) {
+            mParams.add(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, String value) {
+            mParams.add(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, List<String> values) {
+            mParams.add(key, values);
+            return (T) this;
+        }
+
+        /**
+         * Add parameters to body.
+         */
+        public T params(Params params) {
+            mParams.add(params);
+            return (T) this;
+        }
+
+        /**
+         * Set parameters to body.
+         */
+        public T setParams(Params params) {
+            mParams.set(params);
+            return (T) this;
+        }
+
+        @Override
+        public T removeParam(String key) {
+            mParams.remove(key);
+            return (T) this;
+        }
+
+        @Override
+        public T clearParams() {
+            mParams.clear();
+            return (T) this;
+        }
+
+        /**
+         * Add several file parameters.
+         */
+        public T file(String key, File file) {
+            mParams.file(key, file);
+            return (T) this;
+        }
+
+        /**
+         * Add files parameter.
+         */
+        public T files(String key, List<File> files) {
+            mParams.files(key, files);
+            return (T) this;
+        }
+
+        /**
+         * Add binary parameter.
+         */
+        public T binary(String key, Binary binary) {
+            mParams.binary(key, binary);
+            return (T) this;
+        }
+
+        /**
+         * Add several binary parameters.
+         */
+        public T binaries(String key, List<Binary> binaries) {
+            mParams.binaries(key, binaries);
+            return (T) this;
+        }
+
+        /**
+         * Set request body.
+         */
+        public T body(RequestBody body) {
+            this.mBody = body;
+            return (T) this;
+        }
+    }
+
+    public static class Builder extends BodyRequest.Api<BodyRequest.Builder> {
+
+        private Builder(Url url, RequestMethod method) {
+            super(url, method);
+        }
+
+        public BodyRequest build() {
+            return new BodyRequest(this);
+        }
+    }
+
+}

+ 65 - 0
kalle/src/main/java/com/yanzhenjie/kalle/CancelerManager.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/27.
+ */
+public class CancelerManager {
+
+    private final Map<Request, Canceller> mCancelMap;
+
+    public CancelerManager() {
+        this.mCancelMap = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * Add a task to cancel.
+     *
+     * @param request   target request.
+     * @param canceller canceller.
+     */
+    public void addCancel(Request request, Canceller canceller) {
+        mCancelMap.put(request, canceller);
+    }
+
+    /**
+     * Remove a task.
+     *
+     * @param request target request.
+     */
+    public void removeCancel(Request request) {
+        mCancelMap.remove(request);
+    }
+
+    /**
+     * According to the tag to cancel a task.
+     *
+     * @param tag tag.
+     */
+    public void cancel(Object tag) {
+        for (Map.Entry<Request, Canceller> entry : mCancelMap.entrySet()) {
+            Request request = entry.getKey();
+            Object oldTag = request.tag();
+            if ((tag == oldTag) || (tag != null && tag.equals(oldTag))) {
+                entry.getValue().cancel();
+            }
+        }
+    }
+}

+ 31 - 0
kalle/src/main/java/com/yanzhenjie/kalle/Canceller.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/27.
+ */
+public interface Canceller {
+    /**
+     * Cancel operation.
+     */
+    void cancel();
+
+    /**
+     * Operation is canceled.
+     */
+    boolean isCancelled();
+}

+ 40 - 0
kalle/src/main/java/com/yanzhenjie/kalle/Content.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public interface Content {
+
+    /**
+     * Returns the length of the content.
+     */
+    long contentLength();
+
+    /**
+     * Get the type of the content.
+     */
+    String contentType();
+
+    /**
+     * Content data.
+     */
+    void writeTo(OutputStream writer) throws IOException;
+}

+ 70 - 0
kalle/src/main/java/com/yanzhenjie/kalle/FileBinary.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * <p>
+ * A default implementation of Binary.
+ * All the methods are called in Son thread.
+ * </p>
+ * Created in Oct 17, 2015 12:40:54 PM.
+ */
+public class FileBinary extends BaseContent<FileBinary> implements Binary {
+
+    private File mFile;
+
+    public FileBinary(File file) {
+        this.mFile = file;
+    }
+
+    @Override
+    public long contentLength() {
+        return mFile.length();
+    }
+
+    @Override
+    public String name() {
+        return mFile.getName();
+    }
+
+    @Override
+    public String contentType() {
+        String fileName = mFile.getName();
+        String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
+        String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+        if (TextUtils.isEmpty(mimeType)) mimeType = Headers.VALUE_APPLICATION_STREAM;
+        return mimeType;
+    }
+
+
+    @Override
+    protected void onWrite(OutputStream writer) throws IOException {
+        InputStream stream = new FileInputStream(mFile);
+        IOUtils.write(stream, writer);
+        IOUtils.closeQuietly(stream);
+    }
+}

+ 61 - 0
kalle/src/main/java/com/yanzhenjie/kalle/FileBody.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public class FileBody extends BaseContent<FileBody> implements RequestBody {
+
+    private final File mFile;
+
+    public FileBody(File file) {
+        this.mFile = file;
+    }
+
+    @Override
+    public long contentLength() {
+        return mFile.length();
+    }
+
+    @Override
+    public String contentType() {
+        String fileName = mFile.getName();
+        String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
+        String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+        if (TextUtils.isEmpty(mimeType)) mimeType = Headers.VALUE_APPLICATION_STREAM;
+        return mimeType;
+    }
+
+
+    @Override
+    protected void onWrite(OutputStream writer) throws IOException {
+        InputStream reader = new FileInputStream(mFile);
+        IOUtils.write(reader, writer);
+        IOUtils.closeQuietly(reader);
+    }
+}

+ 296 - 0
kalle/src/main/java/com/yanzhenjie/kalle/FormBody.java

@@ -0,0 +1,296 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.util.IOUtils;
+import com.yanzhenjie.kalle.util.LengthOutputStream;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Set;
+
+import static com.yanzhenjie.kalle.Headers.VALUE_APPLICATION_FORM;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/9.
+ */
+public class FormBody extends BaseContent<FormBody> implements RequestBody {
+
+    private final Charset mCharset;
+    private final String mContentType;
+    private final Params mParams;
+    private String mBoundary;
+
+    private FormBody(Builder builder) {
+        this.mCharset = builder.mCharset == null ? Kalle.getConfig().getCharset() : builder.mCharset;
+        this.mContentType = TextUtils.isEmpty(builder.mContentType) ? VALUE_APPLICATION_FORM : builder.mContentType;
+        this.mParams = builder.mParams.build();
+        this.mBoundary = createBoundary();
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    private static String createBoundary() {
+        StringBuilder sb = new StringBuilder("-------FormBoundary");
+        for (int t = 1; t < 12; t++) {
+            long time = System.currentTimeMillis() + t;
+            if (time % 3L == 0L) {
+                sb.append((char) (int) time % '\t');
+            } else if (time % 3L == 1L) {
+                sb.append((char) (int) (65L + time % 26L));
+            } else {
+                sb.append((char) (int) (97L + time % 26L));
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Copy parameters from form body.
+     */
+    public Params copyParams() {
+        return mParams;
+    }
+
+    @Override
+    public long contentLength() {
+        LengthOutputStream stream = new LengthOutputStream();
+        try {
+            onWrite(stream);
+        } catch (IOException ignored) {
+        }
+        return stream.getLength();
+    }
+
+    @Override
+    public String contentType() {
+        return mContentType + "; boundary=" + mBoundary;
+    }
+
+    @Override
+    protected void onWrite(OutputStream writer) throws IOException {
+        Set<String> keys = mParams.keySet();
+        for (String key : keys) {
+            List<Object> values = mParams.get(key);
+            for (Object value : values) {
+                if (value instanceof String) {
+                    writeFormString(writer, key, (String) value);
+                } else if (value instanceof Binary) {
+                    writeFormBinary(writer, key, (Binary) value);
+                }
+            }
+        }
+
+        IOUtils.write(writer, "\r\n", mCharset);
+        IOUtils.write(writer, "--" + mBoundary + "--\r\n", mCharset);
+    }
+
+    private void writeFormString(OutputStream writer, String key, String value) throws IOException {
+        IOUtils.write(writer, "--" + mBoundary + "\r\n", mCharset);
+        IOUtils.write(writer, "Content-Disposition: form-data; name=\"" + key + "\"", mCharset);
+        IOUtils.write(writer, "\r\n\r\n", mCharset);
+        IOUtils.write(writer, value, mCharset);
+        IOUtils.write(writer, "\r\n", mCharset);
+    }
+
+    private void writeFormBinary(OutputStream writer, String key, Binary value) throws IOException {
+        IOUtils.write(writer, "--" + mBoundary + "\r\n", mCharset);
+        IOUtils.write(writer, "Content-Disposition: form-data; name=\"" + key + "\"", mCharset);
+        IOUtils.write(writer, "; filename=\"" + value.name() + "\"", mCharset);
+        IOUtils.write(writer, "\r\n", mCharset);
+        IOUtils.write(writer, "Content-Type: " + value.contentType() + "\r\n\r\n", mCharset);
+        if (writer instanceof LengthOutputStream) {
+            ((LengthOutputStream) writer).write(value.contentLength());
+        } else {
+            value.writeTo(writer);
+        }
+        IOUtils.write(writer, "\r\n", mCharset);
+    }
+
+    public static class Builder {
+
+        private Charset mCharset;
+        private String mContentType;
+        private Params.Builder mParams;
+
+        private Builder() {
+            this.mParams = Params.newBuilder();
+        }
+
+        /**
+         * Data charset.
+         */
+        public Builder charset(Charset charset) {
+            this.mCharset = charset;
+            return this;
+        }
+
+        /**
+         * Content type.
+         */
+        public Builder contentType(String contentType) {
+            this.mContentType = contentType;
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, int value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, long value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, boolean value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, char value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, double value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, float value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, short value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, CharSequence value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, String value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameters.
+         */
+        public Builder param(String key, List<String> values) {
+            mParams.add(key, values);
+            return this;
+        }
+
+        /**
+         * Add parameters.
+         */
+        public Builder params(Params params) {
+            mParams.add(params);
+            return this;
+        }
+
+        /**
+         * Add several file parameters.
+         */
+        public Builder file(String key, File file) {
+            mParams.file(key, file);
+            return this;
+        }
+
+        /**
+         * Add files parameter.
+         */
+        public Builder files(String key, List<File> files) {
+            mParams.files(key, files);
+            return this;
+        }
+
+        /**
+         * Add binary parameter.
+         */
+        public Builder binary(String key, Binary binary) {
+            mParams.binary(key, binary);
+            return this;
+        }
+
+        /**
+         * Add several binary parameters.
+         */
+        public Builder binaries(String key, List<Binary> binaries) {
+            mParams.binaries(key, binaries);
+            return this;
+        }
+
+        /**
+         * Remove parameters.
+         */
+        public Builder removeParam(String key) {
+            mParams.remove(key);
+            return this;
+        }
+
+        /**
+         * Clear parameters.
+         */
+        public Builder clearParams() {
+            mParams.clear();
+            return this;
+        }
+
+        public FormBody build() {
+            return new FormBody(this);
+        }
+    }
+}

+ 515 - 0
kalle/src/main/java/com/yanzhenjie/kalle/Headers.java

@@ -0,0 +1,515 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import android.os.Build;
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.util.ListMap;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.HttpCookie;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+import java.util.TreeMap;
+
+/**
+ * <p>
+ * Http header.
+ * </p>
+ * Created in Jan 10, 2016 2:29:42 PM.
+ */
+public class Headers extends ListMap<String, String> {
+
+    public static final String TIME_FORMAT_HTTP = "EEE, dd MMM y HH:mm:ss 'GMT'";
+    public static final TimeZone TIME_ZONE_GMT = TimeZone.getTimeZone("GMT");
+
+    public static final String KEY_ACCEPT = "Accept";
+    public static final String VALUE_ACCEPT_ALL = "*/*";
+    public static final String KEY_ACCEPT_ENCODING = "Accept-Encoding";
+    public static final String VALUE_ACCEPT_ENCODING = "gzip, deflate";
+    public static final String KEY_ACCEPT_LANGUAGE = "Accept-Language";
+    public static final String VALUE_ACCEPT_LANGUAGE = getLanguage();
+    public static final String KEY_ACCEPT_RANGE = "Accept-Range";
+    public static final String KEY_COOKIE = "Cookie";
+    public static final String KEY_CONTENT_DISPOSITION = "Content-Disposition";
+    public static final String KEY_CONTENT_ENCODING = "Content-Encoding";
+    public static final String KEY_CONTENT_LENGTH = "Content-Length";
+    public static final String KEY_CONTENT_RANGE = "Content-Range";
+    public static final String KEY_CONTENT_TYPE = "Content-Type";
+    public static final String VALUE_APPLICATION_URLENCODED = "application/x-www-form-urlencoded";
+    public static final String VALUE_APPLICATION_FORM = "multipart/form-data";
+    public static final String VALUE_APPLICATION_STREAM = "application/octet-stream";
+    public static final String VALUE_APPLICATION_JSON = "application/json";
+    public static final String VALUE_APPLICATION_XML = "application/xml";
+    public static final String KEY_CACHE_CONTROL = "Cache-Control";
+    public static final String KEY_CONNECTION = "Connection";
+    public static final String VALUE_KEEP_ALIVE = "keep-alive";
+    public static final String VALUE_CLOSE = "close";
+    public static final String KEY_DATE = "Date";
+    public static final String KEY_EXPIRES = "Expires";
+    public static final String KEY_E_TAG = "ETag";
+    public static final String KEY_HOST = "Host";
+    public static final String KEY_IF_MODIFIED_SINCE = "If-Modified-Since";
+    public static final String KEY_IF_NONE_MATCH = "If-None-Match";
+    public static final String KEY_LAST_MODIFIED = "Last-Modified";
+    public static final String KEY_LOCATION = "Location";
+    public static final String KEY_RANGE = "Range";
+    public static final String KEY_SET_COOKIE = "Set-Cookie";
+    public static final String KEY_USER_AGENT = "User-Agent";
+    public static final String VALUE_USER_AGENT = getUserAgent();
+
+    public Headers() {
+        super(new TreeMap<String, List<String>>(new Comparator<String>() {
+            @Override
+            public int compare(String o1, String o2) {
+                return o1.compareTo(o2);
+            }
+        }));
+    }
+
+    /**
+     * Format to Hump-shaped words.
+     */
+    public static String formatKey(String key) {
+        if (TextUtils.isEmpty(key)) return null;
+
+        key = key.toLowerCase(Locale.ENGLISH);
+        String[] words = key.split("-");
+
+        StringBuilder builder = new StringBuilder();
+        for (String word : words) {
+            String first = word.substring(0, 1);
+            String end = word.substring(1);
+            builder.append(first.toUpperCase(Locale.ENGLISH)).append(end).append("-");
+        }
+        if (builder.length() > 0) {
+            builder.deleteCharAt(builder.lastIndexOf("-"));
+        }
+        return builder.toString();
+    }
+
+    /**
+     * From the json format String parsing out the {@code Map<String, List<String>>} data.
+     */
+    public static Headers fromJSONString(String jsonString) throws JSONException {
+        Headers headers = new Headers();
+        JSONObject jsonObject = new JSONObject(jsonString);
+        Iterator<String> keySet = jsonObject.keys();
+        while (keySet.hasNext()) {
+            String key = keySet.next();
+            String value = jsonObject.optString(key);
+            JSONArray values = new JSONArray(value);
+            for (int i = 0; i < values.length(); i++) {
+                headers.add(key, values.optString(i));
+            }
+        }
+        return headers;
+    }
+
+    /**
+     * Into a json format string.
+     */
+    public static String toJSONString(Headers headers) {
+        JSONObject jsonObject = new JSONObject();
+        Set<Map.Entry<String, List<String>>> entrySet = headers.entrySet();
+        for (Map.Entry<String, List<String>> entry : entrySet) {
+            String key = entry.getKey();
+            List<String> values = entry.getValue();
+            JSONArray value = new JSONArray(values);
+            try {
+                jsonObject.put(key, value);
+            } catch (JSONException ignored) {
+            }
+        }
+        return jsonObject.toString();
+    }
+
+    /**
+     * Into a single key-value map.
+     */
+    public static Map<String, String> getRequestHeaders(Headers headers) {
+        Map<String, String> headerMap = new LinkedHashMap<>();
+        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+            String key = entry.getKey();
+            List<String> value = entry.getValue();
+            String trueValue = TextUtils.join("; ", value);
+            headerMap.put(key, trueValue);
+        }
+        return headerMap;
+    }
+
+    /**
+     * All the cookies in header information.
+     */
+    public static List<HttpCookie> getHttpCookieList(Headers headers) {
+        List<HttpCookie> cookies = new ArrayList<>();
+        for (String key : headers.keySet()) {
+            if (key.equalsIgnoreCase(KEY_SET_COOKIE)) {
+                List<String> cookieValues = headers.get(key);
+                for (String cookieStr : cookieValues) {
+                    cookies.addAll(HttpCookie.parse(cookieStr));
+                }
+            }
+        }
+        return cookies;
+    }
+
+    /**
+     * A value of the header information.
+     *
+     * @param content      like {@code text/html;charset=utf-8}.
+     * @param key          like {@code charset}.
+     * @param defaultValue list {@code utf-8}.
+     * @return If you have a value key, you will return the parsed value if you don't return the default value.
+     */
+    public static String parseSubValue(String content, String key, String defaultValue) {
+        if (!TextUtils.isEmpty(content) && !TextUtils.isEmpty(key)) {
+            StringTokenizer stringTokenizer = new StringTokenizer(content, ";");
+            while (stringTokenizer.hasMoreElements()) {
+                String valuePair = stringTokenizer.nextToken();
+                int index = valuePair.indexOf('=');
+                if (index > 0) {
+                    String name = valuePair.substring(0, index).trim();
+                    if (key.equalsIgnoreCase(name)) {
+                        defaultValue = valuePair.substring(index + 1).trim();
+                        break;
+                    }
+                }
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Parse the time in GMT format to milliseconds.
+     */
+    public static long formatGMTToMillis(String gmtTime) throws ParseException {
+        SimpleDateFormat formatter = new SimpleDateFormat(TIME_FORMAT_HTTP, Locale.US);
+        formatter.setTimeZone(TIME_ZONE_GMT);
+        Date date = formatter.parse(gmtTime);
+        return date.getTime();
+    }
+
+    /**
+     * Parse the time in milliseconds to GMT format.
+     */
+    public static String formatMillisToGMT(long milliseconds) {
+        Date date = new Date(milliseconds);
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TIME_FORMAT_HTTP, Locale.US);
+        simpleDateFormat.setTimeZone(TIME_ZONE_GMT);
+        return simpleDateFormat.format(date);
+    }
+
+    /**
+     * Analysis the headers of the cache is valid time.
+     *
+     * @param headers http headers header.
+     * @return Time corresponding milliseconds.
+     */
+    public static long analysisCacheExpires(Headers headers) {
+        final long now = System.currentTimeMillis();
+
+        long maxAge = 0;
+        long staleWhileRevalidate = 0;
+
+        String cacheControl = headers.getCacheControl();
+        if (!TextUtils.isEmpty(cacheControl)) {
+            StringTokenizer tokens = new StringTokenizer(cacheControl, ",");
+            while (tokens.hasMoreTokens()) {
+                String token = tokens.nextToken().trim().toLowerCase(Locale.getDefault());
+                if ((token.equals("no-cache") || token.equals("no-store"))) {
+                    return 0;
+                } else if (token.startsWith("max-age=")) {
+                    maxAge = Long.parseLong(token.substring(8)) * 1000L;
+                } else if (token.startsWith("must-revalidate")) {
+                    // If must-revalidate, It must be from the server to validate expired.
+                    return 0;
+                } else if (token.startsWith("stale-while-revalidate=")) {
+                    staleWhileRevalidate = Long.parseLong(token.substring(23)) * 1000L;
+                }
+            }
+        }
+
+        long localExpire = now;// Local expires time of cache.
+
+        // Have CacheControl.
+        if (!TextUtils.isEmpty(cacheControl)) {
+            localExpire += maxAge;
+            if (staleWhileRevalidate > 0) {
+                localExpire += staleWhileRevalidate;
+            }
+
+            return localExpire;
+        }
+
+        final long expires = headers.getExpires();
+        final long date = headers.getDate();
+
+        // If the server through control the cache Expires.
+        if (expires > date) {
+            return now + expires - date;
+        }
+        return 0;
+    }
+
+    /**
+     * Get language.
+     */
+    public static String getLanguage() {
+        Locale locale = Locale.getDefault();
+        String language = locale.getLanguage();
+        String country = locale.getCountry();
+        StringBuilder builder = new StringBuilder(language);
+        if (!TextUtils.isEmpty(country))
+            builder.append('-').append(country).append(',').append(language);
+        return builder.toString();
+    }
+
+    /**
+     * Get User-Agent.
+     */
+    public static String getUserAgent() {
+        String webUserAgent = "Mozilla/5.0 (Linux; U; Android %s) AppleWebKit/534.30 (KHTML, like Gecko) Version/5.0 %sSafari/534.30";
+
+        StringBuffer buffer = new StringBuffer();
+        buffer.append(Build.VERSION.RELEASE).append("; ");
+
+        Locale locale = Locale.getDefault();
+        String language = locale.getLanguage();
+        if (TextUtils.isEmpty(language)) {
+            buffer.append(language.toLowerCase(locale));
+            final String country = locale.getCountry();
+            if (!TextUtils.isEmpty(country)) {
+                buffer.append("-");
+                buffer.append(country.toLowerCase(locale));
+            }
+        } else {
+            buffer.append("en");
+        }
+        if ("REL".equals(Build.VERSION.CODENAME)) {
+            if (Build.MODEL.length() > 0) {
+                buffer.append("; ");
+                buffer.append(Build.MODEL);
+            }
+        }
+        if (Build.ID.length() > 0) {
+            buffer.append(" Api/");
+            buffer.append(Build.ID);
+        }
+        return String.format(webUserAgent, buffer, "Mobile ");
+    }
+
+    @Override
+    public void add(String key, String value) {
+        if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value))
+            super.add(formatKey(key), value);
+    }
+
+    @Override
+    public void set(String key, String value) {
+        if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value))
+            super.set(formatKey(key), value);
+    }
+
+    @Override
+    public void add(String key, List<String> values) {
+        if (!TextUtils.isEmpty(key) && !values.isEmpty())
+            super.add(formatKey(key), values);
+    }
+
+    @Override
+    public void set(String key, List<String> values) {
+        if (!TextUtils.isEmpty(key) && !values.isEmpty())
+            super.set(formatKey(key), values);
+    }
+
+    public void add(Headers headers) {
+        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+            String key = entry.getKey();
+            List<String> values = entry.getValue();
+            for (String value : values) {
+                add(key, value);
+            }
+        }
+    }
+
+    @Override
+    public List<String> remove(String key) {
+        return super.remove(formatKey(key));
+    }
+
+    @Override
+    public List<String> get(String key) {
+        return super.get(formatKey(key));
+    }
+
+    @Override
+    public String getFirst(String key) {
+        return super.getFirst(formatKey(key));
+    }
+
+    @Override
+    public boolean containsKey(String key) {
+        return super.containsKey(formatKey(key));
+    }
+
+    /**
+     * Replace all.
+     */
+    public void set(Headers headers) {
+        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+            set(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /**
+     * {@value #KEY_CACHE_CONTROL}.
+     *
+     * @return CacheControl.
+     */
+    public String getCacheControl() {
+        List<String> cacheControls = get(KEY_CACHE_CONTROL);
+        if (cacheControls == null) cacheControls = Collections.emptyList();
+        return TextUtils.join(",", cacheControls);
+    }
+
+    /**
+     * {@value KEY_CONTENT_DISPOSITION}.
+     *
+     * @return {@value KEY_CONTENT_DISPOSITION}.
+     */
+    public String getContentDisposition() {
+        return getFirst(KEY_CONTENT_DISPOSITION);
+    }
+
+    /**
+     * {@value #KEY_CONTENT_ENCODING}.
+     *
+     * @return ContentEncoding.
+     */
+    public String getContentEncoding() {
+        return getFirst(KEY_CONTENT_ENCODING);
+    }
+
+    /**
+     * {@value #KEY_CONTENT_LENGTH}.
+     *
+     * @return ContentLength.
+     */
+    public long getContentLength() {
+        String contentLength = getFirst(KEY_CONTENT_LENGTH);
+        if (TextUtils.isEmpty(contentLength)) contentLength = "0";
+        return Long.parseLong(contentLength);
+    }
+
+    /**
+     * {@value #KEY_CONTENT_TYPE}.
+     *
+     * @return ContentType.
+     */
+    public String getContentType() {
+        return getFirst(KEY_CONTENT_TYPE);
+    }
+
+    /**
+     * {@value #KEY_CONTENT_RANGE}.
+     *
+     * @return ContentRange.
+     */
+    public String getContentRange() {
+        return getFirst(KEY_CONTENT_RANGE);
+    }
+
+    /**
+     * {@value #KEY_DATE}.
+     *
+     * @return Date.
+     */
+    public long getDate() {
+        return getDateField(KEY_DATE);
+    }
+
+    /**
+     * {@value #KEY_E_TAG}.
+     *
+     * @return ETag.
+     */
+    public String getETag() {
+        return getFirst(KEY_E_TAG);
+    }
+
+    /**
+     * {@value #KEY_EXPIRES}.
+     *
+     * @return Expiration.
+     */
+    public long getExpires() {
+        return getDateField(KEY_EXPIRES);
+    }
+
+    /**
+     * {@value #KEY_LAST_MODIFIED}.
+     *
+     * @return LastModified.
+     */
+    public long getLastModified() {
+        return getDateField(KEY_LAST_MODIFIED);
+    }
+
+    /**
+     * {@value #KEY_LOCATION}.
+     *
+     * @return Location.
+     */
+    public String getLocation() {
+        return getFirst(KEY_LOCATION);
+    }
+
+    /**
+     * <p>
+     * Returns the date value in milliseconds since 1970.1.1, 00:00h corresponding to the header field field. The
+     * defaultValue will be returned if no such field can be found in the headers header.
+     * </p>
+     *
+     * @param key the header field name.
+     * @return the header field represented in milliseconds since January 1, 1970 GMT.
+     */
+    private long getDateField(String key) {
+        String value = getFirst(key);
+        if (!TextUtils.isEmpty(value))
+            try {
+                return formatGMTToMillis(value);
+            } catch (ParseException ignored) {
+            }
+        return 0;
+    }
+}

+ 34 - 0
kalle/src/main/java/com/yanzhenjie/kalle/JsonBody.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import java.nio.charset.Charset;
+
+import static com.yanzhenjie.kalle.Headers.VALUE_APPLICATION_JSON;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public class JsonBody extends StringBody {
+
+    public JsonBody(String body) {
+        this(body, Kalle.getConfig().getCharset());
+    }
+
+    public JsonBody(String body, Charset charset) {
+        super(body, charset, VALUE_APPLICATION_JSON);
+    }
+}

+ 193 - 0
kalle/src/main/java/com/yanzhenjie/kalle/Kalle.java

@@ -0,0 +1,193 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import android.util.Log;
+
+import com.yanzhenjie.kalle.download.BodyDownload;
+import com.yanzhenjie.kalle.download.DownloadManager;
+import com.yanzhenjie.kalle.download.UrlDownload;
+import com.yanzhenjie.kalle.simple.RequestManager;
+import com.yanzhenjie.kalle.simple.SimpleBodyRequest;
+import com.yanzhenjie.kalle.simple.SimpleUrlRequest;
+
+/**
+ * <p>
+ * Kalle.
+ * </p>
+ * Created in Jul 28, 2015 7:32:22 PM.
+ */
+public final class Kalle {
+
+    private static KalleConfig sConfig;
+
+    private Kalle() {
+    }
+
+    public static KalleConfig getConfig() {
+        setConfig(null);
+        return sConfig;
+    }
+
+    public static void setConfig(KalleConfig config) {
+        if (sConfig == null) {
+            synchronized (KalleConfig.class) {
+                if (sConfig == null)
+                    sConfig = config == null ? KalleConfig.newBuilder().build() : config;
+                else Log.w("Kalle", new IllegalStateException("Only allowed to configure once."));
+            }
+        }
+    }
+
+    public static SimpleUrlRequest.Api get(String url) {
+        return SimpleUrlRequest.newApi(Url.newBuilder(url).build(), RequestMethod.GET);
+    }
+
+    public static SimpleUrlRequest.Api get(Url url) {
+        return SimpleUrlRequest.newApi(url, RequestMethod.GET);
+    }
+
+    public static SimpleUrlRequest.Api head(String url) {
+        return SimpleUrlRequest.newApi(Url.newBuilder(url).build(), RequestMethod.HEAD);
+    }
+
+    public static SimpleUrlRequest.Api head(Url url) {
+        return SimpleUrlRequest.newApi(url, RequestMethod.HEAD);
+    }
+
+    public static SimpleUrlRequest.Api options(String url) {
+        return SimpleUrlRequest.newApi(Url.newBuilder(url).build(), RequestMethod.OPTIONS);
+    }
+
+    public static SimpleUrlRequest.Api options(Url url) {
+        return SimpleUrlRequest.newApi(url, RequestMethod.OPTIONS);
+    }
+
+    public static SimpleUrlRequest.Api trace(String url) {
+        return SimpleUrlRequest.newApi(Url.newBuilder(url).build(), RequestMethod.TRACE);
+    }
+
+    public static SimpleUrlRequest.Api trace(Url url) {
+        return SimpleUrlRequest.newApi(url, RequestMethod.TRACE);
+    }
+
+    public static SimpleBodyRequest.Api post(String url) {
+        return SimpleBodyRequest.newApi(Url.newBuilder(url).build(), RequestMethod.POST);
+    }
+
+    public static SimpleBodyRequest.Api post(Url url) {
+        return SimpleBodyRequest.newApi(url, RequestMethod.POST);
+    }
+
+    public static SimpleBodyRequest.Api put(String url) {
+        return SimpleBodyRequest.newApi(Url.newBuilder(url).build(), RequestMethod.PUT);
+    }
+
+    public static SimpleBodyRequest.Api put(Url url) {
+        return SimpleBodyRequest.newApi(url, RequestMethod.PUT);
+    }
+
+    public static SimpleBodyRequest.Api patch(String url) {
+        return SimpleBodyRequest.newApi(Url.newBuilder(url).build(), RequestMethod.PATCH);
+    }
+
+    public static SimpleBodyRequest.Api patch(Url url) {
+        return SimpleBodyRequest.newApi(url, RequestMethod.PATCH);
+    }
+
+    public static SimpleBodyRequest.Api delete(String url) {
+        return SimpleBodyRequest.newApi(Url.newBuilder(url).build(), RequestMethod.DELETE);
+    }
+
+    public static SimpleBodyRequest.Api delete(Url url) {
+        return SimpleBodyRequest.newApi(url, RequestMethod.DELETE);
+    }
+
+    public static void cancel(Object tag) {
+        RequestManager.getInstance().cancel(tag);
+    }
+
+    public static class Download {
+
+        public static UrlDownload.Api get(String url) {
+            return UrlDownload.newApi(Url.newBuilder(url).build(), RequestMethod.GET);
+        }
+
+        public static UrlDownload.Api get(Url url) {
+            return UrlDownload.newApi(url, RequestMethod.GET);
+        }
+
+        public static UrlDownload.Api head(String url) {
+            return UrlDownload.newApi(Url.newBuilder(url).build(), RequestMethod.HEAD);
+        }
+
+        public static UrlDownload.Api head(Url url) {
+            return UrlDownload.newApi(url, RequestMethod.HEAD);
+        }
+
+        public static UrlDownload.Api options(String url) {
+            return UrlDownload.newApi(Url.newBuilder(url).build(), RequestMethod.OPTIONS);
+        }
+
+        public static UrlDownload.Api options(Url url) {
+            return UrlDownload.newApi(url, RequestMethod.OPTIONS);
+        }
+
+        public static UrlDownload.Api trace(String url) {
+            return UrlDownload.newApi(Url.newBuilder(url).build(), RequestMethod.TRACE);
+        }
+
+        public static UrlDownload.Api trace(Url url) {
+            return UrlDownload.newApi(url, RequestMethod.TRACE);
+        }
+
+        public static BodyDownload.Api post(String url) {
+            return BodyDownload.newApi(Url.newBuilder(url).build(), RequestMethod.POST);
+        }
+
+        public static BodyDownload.Api post(Url url) {
+            return BodyDownload.newApi(url, RequestMethod.POST);
+        }
+
+        public static BodyDownload.Api put(String url) {
+            return BodyDownload.newApi(Url.newBuilder(url).build(), RequestMethod.PUT);
+        }
+
+        public static BodyDownload.Api put(Url url) {
+            return BodyDownload.newApi(url, RequestMethod.PUT);
+        }
+
+        public static BodyDownload.Api patch(String url) {
+            return BodyDownload.newApi(Url.newBuilder(url).build(), RequestMethod.PATCH);
+        }
+
+        public static BodyDownload.Api patch(Url url) {
+            return BodyDownload.newApi(url, RequestMethod.PATCH);
+        }
+
+        public static BodyDownload.Api delete(String url) {
+            return BodyDownload.newApi(Url.newBuilder(url).build(), RequestMethod.DELETE);
+        }
+
+        public static BodyDownload.Api delete(Url url) {
+            return BodyDownload.newApi(url, RequestMethod.DELETE);
+        }
+
+        public static void cancel(Object tag) {
+            DownloadManager.getInstance().cancel(tag);
+        }
+    }
+}

+ 354 - 0
kalle/src/main/java/com/yanzhenjie/kalle/KalleConfig.java

@@ -0,0 +1,354 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import com.yanzhenjie.kalle.connect.ConnectFactory;
+import com.yanzhenjie.kalle.connect.Interceptor;
+import com.yanzhenjie.kalle.connect.Network;
+import com.yanzhenjie.kalle.cookie.CookieStore;
+import com.yanzhenjie.kalle.simple.Converter;
+import com.yanzhenjie.kalle.simple.cache.CacheStore;
+import com.yanzhenjie.kalle.ssl.SSLUtils;
+import com.yanzhenjie.kalle.urlconnect.URLConnectionFactory;
+import com.yanzhenjie.kalle.util.MainExecutor;
+import com.yanzhenjie.kalle.util.WorkExecutor;
+
+import java.net.Proxy;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+
+import static com.yanzhenjie.kalle.Headers.KEY_ACCEPT;
+import static com.yanzhenjie.kalle.Headers.KEY_ACCEPT_ENCODING;
+import static com.yanzhenjie.kalle.Headers.KEY_ACCEPT_LANGUAGE;
+import static com.yanzhenjie.kalle.Headers.KEY_CONNECTION;
+import static com.yanzhenjie.kalle.Headers.KEY_CONTENT_TYPE;
+import static com.yanzhenjie.kalle.Headers.KEY_USER_AGENT;
+import static com.yanzhenjie.kalle.Headers.VALUE_ACCEPT_ALL;
+import static com.yanzhenjie.kalle.Headers.VALUE_ACCEPT_ENCODING;
+import static com.yanzhenjie.kalle.Headers.VALUE_ACCEPT_LANGUAGE;
+import static com.yanzhenjie.kalle.Headers.VALUE_APPLICATION_URLENCODED;
+import static com.yanzhenjie.kalle.Headers.VALUE_KEEP_ALIVE;
+import static com.yanzhenjie.kalle.Headers.VALUE_USER_AGENT;
+
+/**
+ * <p>Initialize the save parameters.</p>
+ * Created by Zhenjie Yan on 2017/6/14.
+ */
+public final class KalleConfig {
+
+    private final Executor mWorkExecutor;
+    private final Executor mMainExecutor;
+    private final Charset mCharset;
+    private final Headers mHeaders;
+    private final Proxy mProxy;
+    private final SSLSocketFactory mSSLSocketFactory;
+    private final HostnameVerifier mHostnameVerifier;
+    private final int mConnectTimeout;
+    private final int mReadTimeout;
+    private final Params mParams;
+    private final CacheStore mCacheStore;
+    private final Network mNetwork;
+    private final ConnectFactory mConnectFactory;
+    private final CookieStore mCookieStore;
+    private final List<Interceptor> mInterceptors;
+    private final Converter mConverter;
+
+    private KalleConfig(Builder builder) {
+        this.mWorkExecutor = builder.mWorkExecutor == null ? new WorkExecutor() : builder.mWorkExecutor;
+        this.mMainExecutor = builder.mMainExecutor == null ? new MainExecutor() : builder.mMainExecutor;
+
+        this.mCharset = builder.mCharset == null ? Charset.defaultCharset() : builder.mCharset;
+        this.mHeaders = builder.mHeaders;
+        this.mProxy = builder.mProxy;
+        this.mSSLSocketFactory = builder.mSSLSocketFactory == null ? SSLUtils.SSL_SOCKET_FACTORY : builder.mSSLSocketFactory;
+        this.mHostnameVerifier = builder.mHostnameVerifier == null ? SSLUtils.HOSTNAME_VERIFIER : builder.mHostnameVerifier;
+        this.mConnectTimeout = builder.mConnectTimeout <= 0 ? 10000 : builder.mConnectTimeout;
+        this.mReadTimeout = builder.mReadTimeout <= 0 ? 10000 : builder.mReadTimeout;
+        this.mParams = builder.mParams.build();
+
+        this.mCacheStore = builder.mCacheStore == null ? CacheStore.DEFAULT : builder.mCacheStore;
+
+        this.mNetwork = builder.mNetwork == null ? Network.DEFAULT : builder.mNetwork;
+        this.mConnectFactory = builder.mConnectFactory == null ? URLConnectionFactory.newBuilder().build() : builder.mConnectFactory;
+        this.mCookieStore = builder.mCookieStore == null ? CookieStore.DEFAULT : builder.mCookieStore;
+        this.mInterceptors = Collections.unmodifiableList(builder.mInterceptors);
+
+        this.mConverter = builder.mConverter == null ? Converter.DEFAULT : builder.mConverter;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public Executor getWorkExecutor() {
+        return mWorkExecutor;
+    }
+
+    public Executor getMainExecutor() {
+        return mMainExecutor;
+    }
+
+    public Charset getCharset() {
+        return mCharset;
+    }
+
+    public Headers getHeaders() {
+        return mHeaders;
+    }
+
+    public Proxy getProxy() {
+        return mProxy;
+    }
+
+    public SSLSocketFactory getSSLSocketFactory() {
+        return mSSLSocketFactory;
+    }
+
+    public HostnameVerifier getHostnameVerifier() {
+        return mHostnameVerifier;
+    }
+
+    public int getConnectTimeout() {
+        return mConnectTimeout;
+    }
+
+    public int getReadTimeout() {
+        return mReadTimeout;
+    }
+
+    public Params getParams() {
+        return mParams;
+    }
+
+    public CacheStore getCacheStore() {
+        return mCacheStore;
+    }
+
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    public ConnectFactory getConnectFactory() {
+        return mConnectFactory;
+    }
+
+    public CookieStore getCookieStore() {
+        return mCookieStore;
+    }
+
+    public List<Interceptor> getInterceptor() {
+        return mInterceptors;
+    }
+
+    public Converter getConverter() {
+        return mConverter;
+    }
+
+    public final static class Builder {
+
+        private Executor mWorkExecutor;
+        private Executor mMainExecutor;
+
+        private Charset mCharset;
+        private Headers mHeaders;
+        private Proxy mProxy;
+        private SSLSocketFactory mSSLSocketFactory;
+        private HostnameVerifier mHostnameVerifier;
+        private int mConnectTimeout;
+        private int mReadTimeout;
+        private Params.Builder mParams;
+
+        private CacheStore mCacheStore;
+
+        private Network mNetwork;
+        private ConnectFactory mConnectFactory;
+        private CookieStore mCookieStore;
+        private List<Interceptor> mInterceptors;
+
+        private Converter mConverter;
+
+        private Builder() {
+            this.mHeaders = new Headers();
+            this.mParams = Params.newBuilder();
+            this.mInterceptors = new ArrayList<>();
+
+            mHeaders.set(KEY_ACCEPT, VALUE_ACCEPT_ALL);
+            mHeaders.set(KEY_ACCEPT_ENCODING, VALUE_ACCEPT_ENCODING);
+            mHeaders.set(KEY_CONTENT_TYPE, VALUE_APPLICATION_URLENCODED);
+            mHeaders.set(KEY_CONNECTION, VALUE_KEEP_ALIVE);
+            mHeaders.set(KEY_USER_AGENT, VALUE_USER_AGENT);
+            mHeaders.set(KEY_ACCEPT_LANGUAGE, VALUE_ACCEPT_LANGUAGE);
+        }
+
+        /**
+         * Set global work thread executor.
+         */
+        public Builder workThreadExecutor(Executor executor) {
+            this.mWorkExecutor = executor;
+            return this;
+        }
+
+        /**
+         * Set global main thread executor.
+         */
+        public Builder mainThreadExecutor(Executor executor) {
+            this.mMainExecutor = executor;
+            return this;
+        }
+
+        /**
+         * Global charset.
+         */
+        public Builder charset(Charset charset) {
+            this.mCharset = charset;
+            return this;
+        }
+
+        /**
+         * Add the global header.
+         */
+        public Builder addHeader(String key, String value) {
+            mHeaders.add(key, value);
+            return this;
+        }
+
+        /**
+         * Set the global header.
+         */
+        public Builder setHeader(String key, String value) {
+            mHeaders.set(key, value);
+            return this;
+        }
+
+        /**
+         * Global proxy.
+         */
+        public Builder proxy(Proxy proxy) {
+            this.mProxy = proxy;
+            return this;
+        }
+
+        /**
+         * Global ssl socket factory.
+         */
+        public Builder sslSocketFactory(SSLSocketFactory sslSocketFactory) {
+            this.mSSLSocketFactory = sslSocketFactory;
+            return this;
+        }
+
+        /**
+         * Global hostname verifier.
+         */
+        public Builder hostnameVerifier(HostnameVerifier hostnameVerifier) {
+            this.mHostnameVerifier = hostnameVerifier;
+            return this;
+        }
+
+        /**
+         * Global connection timeout.
+         */
+        public Builder connectionTimeout(int timeout, TimeUnit timeUnit) {
+            long time = timeUnit.toMillis(timeout);
+            this.mConnectTimeout = (int) Math.min(time, Integer.MAX_VALUE);
+            return this;
+        }
+
+        /**
+         * Global readResponse timeout.
+         */
+        public Builder readTimeout(int timeout, TimeUnit timeUnit) {
+            long time = timeUnit.toMillis(timeout);
+            this.mReadTimeout = (int) Math.min(time, Integer.MAX_VALUE);
+            return this;
+        }
+
+        /**
+         * Add the global param.
+         */
+        public Builder addParam(String key, String value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Global cache store.
+         */
+        public Builder cacheStore(CacheStore cacheStore) {
+            this.mCacheStore = cacheStore;
+            return this;
+        }
+
+        /**
+         * Global network.
+         */
+        public Builder network(Network network) {
+            this.mNetwork = network;
+            return this;
+        }
+
+        /**
+         * Global cookie store.
+         */
+        public Builder connectFactory(ConnectFactory factory) {
+            this.mConnectFactory = factory;
+            return this;
+        }
+
+        /**
+         * Global cookie store.
+         */
+        public Builder cookieStore(CookieStore cookieStore) {
+            this.mCookieStore = cookieStore;
+            return this;
+        }
+
+        /**
+         * Add the global interceptor.
+         */
+        public Builder addInterceptor(Interceptor interceptor) {
+            this.mInterceptors.add(interceptor);
+            return this;
+        }
+
+        /**
+         * Add the global interceptor.
+         */
+        public Builder addInterceptors(List<Interceptor> interceptors) {
+            this.mInterceptors.addAll(interceptors);
+            return this;
+        }
+
+        /**
+         * The converter.
+         */
+        public Builder converter(Converter converter) {
+            this.mConverter = converter;
+            return this;
+        }
+
+        public KalleConfig build() {
+            return new KalleConfig(this);
+        }
+    }
+
+}

+ 324 - 0
kalle/src/main/java/com/yanzhenjie/kalle/Params.java

@@ -0,0 +1,324 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/23.
+ */
+public class Params {
+
+    private final Map<String, List<Object>> mMap;
+
+    private Params(Builder builder) {
+        this.mMap = builder.mMap;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * Get parameters by key.
+     *
+     * @param key key.
+     * @return if the key does not exist, it may be null.
+     */
+    public List<Object> get(String key) {
+        return mMap.get(key);
+    }
+
+    /**
+     * Get the first value of the key.
+     *
+     * @param key key.
+     * @return if the key does not exist, it may be null.
+     */
+    public Object getFirst(String key) {
+        List<Object> values = mMap.get(key);
+        if (values != null && values.size() > 0) return values.get(0);
+        return null;
+    }
+
+    /**
+     * Get {@link Set} view of the parameters.
+     *
+     * @return a set view of the mappings.
+     * @see Map#entrySet()
+     */
+    public Set<Map.Entry<String, List<Object>>> entrySet() {
+        return mMap.entrySet();
+    }
+
+    /**
+     * Get {@link Set} view of the keys.
+     *
+     * @return a set view of the keys.
+     * @see Map#keySet()
+     */
+    public Set<String> keySet() {
+        return mMap.keySet();
+    }
+
+    /**
+     * No parameters.
+     *
+     * @return true if there are no key-values pairs.
+     * @see Map#isEmpty()
+     */
+    public boolean isEmpty() {
+        return mMap.isEmpty();
+    }
+
+    /**
+     * Parameters contains the key.
+     *
+     * @param key key.
+     * @return true if there contains the key.
+     */
+    public boolean containsKey(String key) {
+        return mMap.containsKey(key);
+    }
+
+    /**
+     * Does it contain {@link Binary}.
+     */
+    public boolean hasBinary() {
+        for (Map.Entry<String, List<Object>> entry : entrySet()) {
+            List<Object> values = entry.getValue();
+            if (values.size() > 0) {
+                for (Object value : values) if (value instanceof Binary) return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Convert to {@link UrlBody}
+     */
+    public UrlBody toUrlBody() {
+        return UrlBody.newBuilder().params(this).build();
+    }
+
+    /**
+     * Convert to {@link FormBody}
+     */
+    public FormBody toFormBody() {
+        return FormBody.newBuilder().params(this).build();
+    }
+
+    /**
+     * ReBuilder.
+     */
+    public Builder builder() {
+        Map<String, List<Object>> map = new LinkedHashMap<>();
+        for (Map.Entry<String, List<Object>> entry : mMap.entrySet()) {
+            map.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+        }
+        return new Builder(map);
+    }
+
+    @Override
+    public String toString() {
+        return toString(false);
+    }
+
+    public String toString(boolean encode) {
+        StringBuilder builder = new StringBuilder();
+        Set<String> keySet = keySet();
+        for (String key : keySet) {
+            List<Object> values = get(key);
+            for (Object value : values) {
+                if (value instanceof CharSequence) {
+                    String textValue = encode ? Uri.encode(value.toString()) : value.toString();
+                    builder.append("&").append(key).append("=").append(textValue);
+                }
+            }
+        }
+        if (builder.length() > 0) builder.deleteCharAt(0);
+        return builder.toString();
+    }
+
+    public static class Builder {
+
+        private Map<String, List<Object>> mMap;
+
+        private Builder() {
+            this.mMap = new LinkedHashMap<>();
+        }
+
+        private Builder(Map<String, List<Object>> map) {
+            this.mMap = map;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder add(String key, int value) {
+            return add(key, Integer.toString(value));
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder add(String key, long value) {
+            return add(key, Long.toString(value));
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder add(String key, boolean value) {
+            return add(key, Boolean.toString(value));
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder add(String key, char value) {
+            return add(key, String.valueOf(value));
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder add(String key, double value) {
+            return add(key, Double.toString(value));
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder add(String key, float value) {
+            return add(key, Float.toString(value));
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder add(String key, short value) {
+            return add(key, Integer.toString(value));
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder add(String key, CharSequence value) {
+            return add(key, (Object) value);
+        }
+
+        /**
+         * Add parameters.
+         */
+        public Builder add(String key, List<String> values) {
+            for (String value : values) add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        private Builder add(String key, Object value) {
+            if (!TextUtils.isEmpty(key)) {
+                if (!mMap.containsKey(key)) {
+                    mMap.put(key, new ArrayList<>(1));
+                }
+                if (value == null) value = "";
+                if (value instanceof File) value = new FileBinary((File) value);
+                mMap.get(key).add(value);
+            }
+            return this;
+        }
+
+        /**
+         * Add parameters.
+         */
+        public Builder add(Params params) {
+            for (Map.Entry<String, List<Object>> entry : params.entrySet()) {
+                String key = entry.getKey();
+                List<Object> valueList = entry.getValue();
+                for (Object value : valueList) add(key, value);
+            }
+            return this;
+        }
+
+        /**
+         * Add parameters.
+         */
+        public Builder set(Params params) {
+            return clear().add(params);
+        }
+
+        /**
+         * Add a file parameter.
+         */
+        public Builder file(String key, File file) {
+            return add(key, file);
+        }
+
+        /**
+         * Add several file parameters.
+         */
+        public Builder files(String key, List<File> files) {
+            for (File file : files) add(key, file);
+            return this;
+        }
+
+        /**
+         * Add binary parameter.
+         */
+        public Builder binary(String key, Binary binary) {
+            return add(key, binary);
+        }
+
+        /**
+         * Add several binary parameters.
+         */
+        public Builder binaries(String key, List<Binary> binaries) {
+            for (Binary binary : binaries) binary(key, binary);
+            return this;
+        }
+
+        /**
+         * Remove parameters by key.
+         */
+        public Builder remove(String key) {
+            mMap.remove(key);
+            return this;
+        }
+
+        /**
+         * Remove all parameters.
+         */
+        public Builder clear() {
+            mMap.clear();
+            return this;
+        }
+
+        public Params build() {
+            return new Params(this);
+        }
+    }
+}

+ 26 - 0
kalle/src/main/java/com/yanzhenjie/kalle/ProgressBar.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public interface ProgressBar<T> {
+    /**
+     * Progress has changed.
+     */
+    void progress(T origin, int progress);
+}

+ 319 - 0
kalle/src/main/java/com/yanzhenjie/kalle/Request.java

@@ -0,0 +1,319 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import java.net.Proxy;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Created in Nov 4, 2015 8:28:50 AM.
+ */
+public abstract class Request {
+
+    private final RequestMethod mMethod;
+    private final Headers mHeaders;
+
+    private final Proxy mProxy;
+    private final SSLSocketFactory mSSLSocketFactory;
+    private final HostnameVerifier mHostnameVerifier;
+    private final int mConnectTimeout;
+    private final int mReadTimeout;
+    private final Object mTag;
+
+    protected <T extends Api<T>> Request(Api<T> api) {
+        this.mMethod = api.mMethod;
+        this.mHeaders = api.mHeaders;
+
+        this.mProxy = api.mProxy;
+        this.mSSLSocketFactory = api.mSSLSocketFactory;
+        this.mHostnameVerifier = api.mHostnameVerifier;
+        this.mConnectTimeout = api.mConnectTimeout;
+        this.mReadTimeout = api.mReadTimeout;
+        this.mTag = api.mTag;
+    }
+
+    /**
+     * Get url.
+     */
+    public abstract Url url();
+
+    /**
+     * Get params.
+     */
+    public abstract Params copyParams();
+
+    /**
+     * Get request body.
+     */
+    public abstract RequestBody body();
+
+    /**
+     * Get method.
+     */
+    public RequestMethod method() {
+        return mMethod;
+    }
+
+    /**
+     * Get headers.
+     */
+    public Headers headers() {
+        return mHeaders;
+    }
+
+    /**
+     * Get proxy server.
+     */
+    public Proxy proxy() {
+        return mProxy;
+    }
+
+    /**
+     * Get SSLSocketFactory.
+     */
+    public SSLSocketFactory sslSocketFactory() {
+        return mSSLSocketFactory;
+    }
+
+    /**
+     * Get the HostnameVerifier.
+     */
+    public HostnameVerifier hostnameVerifier() {
+        return mHostnameVerifier;
+    }
+
+    /**
+     * Get the connection timeout time, Unit is a millisecond.
+     */
+    public int connectTimeout() {
+        return mConnectTimeout;
+    }
+
+    /**
+     * Get the readResponse timeout time, Unit is a millisecond.
+     */
+    public int readTimeout() {
+        return mReadTimeout;
+    }
+
+    /**
+     * Get tag.
+     */
+    public Object tag() {
+        return mTag;
+    }
+
+    public static abstract class Api<T extends Api<T>> {
+
+        private final RequestMethod mMethod;
+        private final Headers mHeaders = new Headers();
+        private Proxy mProxy = Kalle.getConfig().getProxy();
+        private SSLSocketFactory mSSLSocketFactory = Kalle.getConfig().getSSLSocketFactory();
+        private HostnameVerifier mHostnameVerifier = Kalle.getConfig().getHostnameVerifier();
+        private int mConnectTimeout = Kalle.getConfig().getConnectTimeout();
+        private int mReadTimeout = Kalle.getConfig().getReadTimeout();
+        private Object mTag;
+
+        protected Api(RequestMethod method) {
+            this.mMethod = method;
+            this.mHeaders.add(Kalle.getConfig().getHeaders());
+        }
+
+        /**
+         * Overlay path.
+         */
+        public abstract T path(int value);
+
+        /**
+         * Overlay path.
+         */
+        public abstract T path(long value);
+
+        /**
+         * Overlay path.
+         */
+        public abstract T path(boolean value);
+
+        /**
+         * Overlay path.
+         */
+        public abstract T path(char value);
+
+        /**
+         * Overlay path.
+         */
+        public abstract T path(double value);
+
+        /**
+         * Overlay path.
+         */
+        public abstract T path(float value);
+
+        /**
+         * Overlay path.
+         */
+        public abstract T path(String value);
+
+        /**
+         * Add a new header.
+         */
+        public T addHeader(String key, String value) {
+            mHeaders.add(key, value);
+            return (T) this;
+        }
+
+        /**
+         * If the target key exists, replace it, if not, add.
+         */
+        public T setHeader(String key, String value) {
+            mHeaders.set(key, value);
+            return (T) this;
+        }
+
+        /**
+         * Set headers.
+         */
+        public T setHeaders(Headers headers) {
+            mHeaders.set(headers);
+            return (T) this;
+        }
+
+        /**
+         * Remove the key from the information.
+         */
+        public T removeHeader(String key) {
+            mHeaders.remove(key);
+            return (T) this;
+        }
+
+        /**
+         * Remove all header.
+         */
+        public T clearHeaders() {
+            mHeaders.clear();
+            return (T) this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public abstract T param(String key, int value);
+
+        /**
+         * Add parameter.
+         */
+        public abstract T param(String key, long value);
+
+        /**
+         * Add parameter.
+         */
+        public abstract T param(String key, boolean value);
+
+        /**
+         * Add parameter.
+         */
+        public abstract T param(String key, char value);
+
+        /**
+         * Add parameter.
+         */
+        public abstract T param(String key, double value);
+
+        /**
+         * Add parameter.
+         */
+        public abstract T param(String key, float value);
+
+        /**
+         * Add parameter.
+         */
+        public abstract T param(String key, short value);
+
+        /**
+         * Add parameter.
+         */
+        public abstract T param(String key, String value);
+
+        /**
+         * Add parameters.
+         */
+        public abstract T param(String key, List<String> value);
+
+        /**
+         * Remove parameters.
+         */
+        public abstract T removeParam(String key);
+
+        /**
+         * Clear parameters.
+         */
+        public abstract T clearParams();
+
+        /**
+         * Proxy information for this request.
+         */
+        public T proxy(Proxy proxy) {
+            this.mProxy = proxy;
+            return (T) this;
+        }
+
+        /**
+         * SSLSocketFactory for this request.
+         */
+        public T sslSocketFactory(SSLSocketFactory sslSocketFactory) {
+            this.mSSLSocketFactory = sslSocketFactory;
+            return (T) this;
+        }
+
+        /**
+         * HostnameVerifier for this request.
+         */
+        public T hostnameVerifier(HostnameVerifier hostnameVerifier) {
+            this.mHostnameVerifier = hostnameVerifier;
+            return (T) this;
+        }
+
+        /**
+         * Connect timeout for this request.
+         */
+        public T connectTimeout(int timeout, TimeUnit timeUnit) {
+            long time = timeUnit.toMillis(timeout);
+            this.mConnectTimeout = (int) Math.min(time, Integer.MAX_VALUE);
+            return (T) this;
+        }
+
+        /**
+         * Read timeout for this request.
+         */
+        public T readTimeout(int timeout, TimeUnit timeUnit) {
+            long time = timeUnit.toMillis(timeout);
+            this.mReadTimeout = (int) Math.min(time, Integer.MAX_VALUE);
+            return (T) this;
+        }
+
+        /**
+         * Tag.
+         */
+        public T tag(Object tag) {
+            this.mTag = tag;
+            return (T) this;
+        }
+    }
+}

+ 22 - 0
kalle/src/main/java/com/yanzhenjie/kalle/RequestBody.java

@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/9.
+ */
+public interface RequestBody extends Content {
+}

+ 102 - 0
kalle/src/main/java/com/yanzhenjie/kalle/RequestMethod.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import java.util.Locale;
+
+/**
+ * <p>
+ * HTTP handle method.
+ * </p>
+ * Created in Oct 10, 2015 8:00:48 PM.
+ */
+public enum RequestMethod {
+
+    GET("GET"),
+
+    POST("POST"),
+
+    PUT("PUT"),
+
+    DELETE("DELETE"),
+
+    HEAD("HEAD"),
+
+    PATCH("PATCH"),
+
+    OPTIONS("OPTIONS"),
+
+    TRACE("TRACE");
+
+    private final String value;
+
+    RequestMethod(String value) {
+        this.value = value;
+    }
+
+    public static RequestMethod reverse(String method) {
+        method = method.toUpperCase(Locale.ENGLISH);
+        switch (method) {
+            case "GET": {
+                return GET;
+            }
+            case "POST": {
+                return POST;
+            }
+            case "PUT": {
+                return PUT;
+            }
+            case "DELETE": {
+                return DELETE;
+            }
+            case "HEAD": {
+                return HEAD;
+            }
+            case "PATCH": {
+                return PATCH;
+            }
+            case "OPTIONS": {
+                return OPTIONS;
+            }
+            case "TRACE": {
+                return TRACE;
+            }
+            default: {
+                return GET;
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return value;
+    }
+
+    public boolean allowBody() {
+        switch (this) {
+            case POST:
+            case PUT:
+            case PATCH:
+            case DELETE: {
+                return true;
+            }
+            default: {
+                return false;
+            }
+        }
+    }
+
+}

+ 114 - 0
kalle/src/main/java/com/yanzhenjie/kalle/Response.java

@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * Created in Oct 15, 2015 8:55:37 PM.
+ */
+public final class Response implements Closeable {
+
+    private final int mCode;
+    private final Headers mHeaders;
+    private final ResponseBody mBody;
+    private Response(Builder builder) {
+        this.mCode = builder.mCode;
+        this.mHeaders = builder.mHeaders;
+        this.mBody = builder.mBody;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * Get the mCode of response.
+     */
+    public int code() {
+        return mCode;
+    }
+
+    /**
+     * Get http headers.
+     */
+    public Headers headers() {
+        return mHeaders;
+    }
+
+    /**
+     * Get http body.
+     */
+    public ResponseBody body() {
+        return mBody;
+    }
+
+    @Override
+    public void close() throws IOException {
+        IOUtils.closeQuietly(mBody);
+    }
+
+    /**
+     * It is a redirect response code.
+     */
+    public boolean isRedirect() {
+        switch (mCode) {
+            case 300:
+            case 301:
+            case 302:
+            case 303:
+            case 307:
+            case 308:
+                return true;
+            case 304:
+            case 305:
+            case 306:
+            default:
+                return false;
+        }
+    }
+
+    public static final class Builder {
+        private int mCode;
+        private Headers mHeaders;
+        private ResponseBody mBody;
+
+        public Builder() {
+        }
+
+        public Builder code(int code) {
+            this.mCode = code;
+            return this;
+        }
+
+        public Builder headers(Headers headers) {
+            this.mHeaders = headers;
+            return this;
+        }
+
+        public Builder body(ResponseBody body) {
+            this.mBody = body;
+            return this;
+        }
+
+        public Response build() {
+            return new Response(this);
+        }
+    }
+}

+ 41 - 0
kalle/src/main/java/com/yanzhenjie/kalle/ResponseBody.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/22.
+ */
+public interface ResponseBody extends Closeable {
+
+    /**
+     * Transform the response data into a string.
+     */
+    String string() throws IOException;
+
+    /**
+     * Transform the response data into a byte array.
+     */
+    byte[] byteArray() throws IOException;
+
+    /**
+     * Transform the response data into a stream.
+     */
+    InputStream stream() throws IOException;
+}

+ 75 - 0
kalle/src/main/java/com/yanzhenjie/kalle/StringBody.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+import static com.yanzhenjie.kalle.Headers.VALUE_APPLICATION_STREAM;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public class StringBody extends BaseContent<StringBody> implements RequestBody {
+
+    private final String mBody;
+    private final Charset mCharset;
+    private final String mContentType;
+
+    public StringBody(String body) {
+        this(body, Kalle.getConfig().getCharset());
+    }
+
+    public StringBody(String body, Charset charset) {
+        this(body, charset, VALUE_APPLICATION_STREAM);
+    }
+
+    public StringBody(String body, String contentType) {
+        this(body, Kalle.getConfig().getCharset(), contentType);
+    }
+
+    public StringBody(String body, Charset charset, String contentType) {
+        this.mBody = body;
+        this.mCharset = charset;
+        this.mContentType = contentType;
+    }
+
+    @Override
+    public long contentLength() {
+        if (TextUtils.isEmpty(mBody)) return 0;
+        return IOUtils.toByteArray(mBody, mCharset).length;
+    }
+
+    @Override
+    public String contentType() {
+        return mContentType + "; charset=" + mCharset.name();
+    }
+
+    @Override
+    protected void onWrite(OutputStream writer) throws IOException {
+        IOUtils.write(writer, mBody, mCharset);
+    }
+
+    @Override
+    public String toString() {
+        return mBody;
+    }
+}

+ 388 - 0
kalle/src/main/java/com/yanzhenjie/kalle/Url.java

@@ -0,0 +1,388 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.webkit.URLUtil;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Add the mPath to the URL, such as:
+ * <pre>
+ * Url url = Url.newBuilder("http://www.example.com/xx")
+ *      .scheme("https")
+ *      .port(8080)
+ *      .path("yy")
+ *      .query("abc", "123")
+ *      .setFragment("article")
+ *      .build();
+ * ...
+ * The real url is: <code>https://www.example.com:8080/xx/yy?abc=123#article</code>.
+ * </pre>
+ * <pre>
+ * Url url = Url.newBuilder("http://www.example.com/xx/yy?abc=123")
+ *      .setPath("/aa/bb/cc")
+ *      .setQuery("mln=456&ijk=789")
+ *      .build();
+ * ...
+ * The real url is: <code>http://www.example.com/aa/bb/cc?mln=456&ijk=789</code>.
+ * </pre>
+ * <pre>
+ * Url url = Url.newBuilder("http://www.example.com/user/photo/search?name=abc").build();
+ * Url newUrl = url.location("../../get?name=mln");
+ * ...
+ * The new url is: <code>http://www.example.com/get?name=abc</code>.
+ * </pre>
+ * Created by Zhenjie Yan on 2018/2/9.
+ */
+public class Url {
+
+    private final String mScheme;
+    private final String mHost;
+    private final int mPort;
+    private final String mPath;
+    private final String mQuery;
+    private final String mFragment;
+    private Url(Builder builder) {
+        this.mScheme = builder.mScheme;
+        this.mHost = builder.mHost;
+        this.mPort = builder.mPort;
+        this.mPath = builder.mPath;
+        this.mQuery = builder.mQuery.build().toString(false);
+        this.mFragment = builder.mFragment;
+    }
+
+    public static Builder newBuilder(String url) {
+        return new Builder(url);
+    }
+
+    private static int convertPort(int port) {
+        return port > 0 ? port : 80;
+    }
+
+    private static List<String> convertPath(String path) {
+        List<String> pathList = new LinkedList<>();
+        if (!TextUtils.isEmpty(path)) {
+            while (path.startsWith("/")) path = path.substring(1);
+            while (path.endsWith("/")) path = path.substring(0, path.length() - 1);
+            String[] pathArray = path.split("/");
+            Collections.addAll(pathList, pathArray);
+        }
+        return pathList;
+    }
+
+    private static Params convertQuery(String query) {
+        Params.Builder params = Params.newBuilder();
+        if (!TextUtils.isEmpty(query)) {
+            if (query.startsWith("?")) query = query.substring(1);
+            String[] paramArray = query.split("&");
+            for (String param : paramArray) {
+                String key;
+                String value = "";
+                int end;
+                if ((end = param.indexOf("=")) > 0) {
+                    key = param.substring(0, end);
+                    if (end < param.length() - 1) {
+                        value = param.substring(end + 1);
+                    }
+                } else {
+                    key = param;
+                }
+                params.add(key, value);
+            }
+        }
+        return params.build();
+    }
+
+    private static String wrapPort(int port) {
+        return (port <= 0 || port == 80) ? "" : String.format(Locale.getDefault(), ":%d", port);
+    }
+
+    private static String wrapPath(List<String> pathList, boolean encode) {
+        if (pathList.isEmpty()) return "/";
+        StringBuilder builder = new StringBuilder();
+        for (String path : pathList) {
+            builder.append("/").append(encode ? Uri.encode(path) : path);
+        }
+        return builder.toString();
+    }
+
+    private static String wrapQuery(Params params, boolean encode) {
+        String query = params.toString(encode);
+        return TextUtils.isEmpty(query) ? "" : String.format("?%s", query);
+    }
+
+    private static String wrapFragment(String fragment, boolean encode) {
+        return TextUtils.isEmpty(fragment) ? "" : String.format("#%s", encode ? Uri.encode(fragment) : fragment);
+    }
+
+    public String getScheme() {
+        return mScheme;
+    }
+
+    public String getHost() {
+        return mHost;
+    }
+
+    public int getPort() {
+        return mPort;
+    }
+
+    public String getPath() {
+        return mPath;
+    }
+
+    public List<String> copyPath() {
+        return convertPath(mPath);
+    }
+
+    public String getQuery() {
+        return mQuery;
+    }
+
+    /**
+     * @deprecated use {@link #getParams()} instead.
+     */
+    @Deprecated
+    public Params copyQuery() {
+        return getParams();
+    }
+
+    public Params getParams() {
+        return convertQuery(mQuery);
+    }
+
+    public String getFragment() {
+        return mFragment;
+    }
+
+    public Builder builder() {
+        return newBuilder(toString());
+    }
+
+    @Override
+    public String toString() {
+        return toString(false);
+    }
+
+    public String toString(boolean encode) {
+        String query = wrapQuery(convertQuery(mQuery), encode);
+        String fragment = wrapFragment(mFragment, encode);
+        return mScheme + "://" + mHost + wrapPort(mPort) + mPath + query + fragment;
+    }
+
+    public Url location(String location) {
+        if (TextUtils.isEmpty(location)) return null;
+
+        if (URLUtil.isNetworkUrl(location)) {
+            return newBuilder(location).build();
+        }
+
+        URI newUri = URI.create(location);
+        if (location.startsWith("/")) {
+            return builder().setPath(newUri.getPath())
+                    .setQuery(newUri.getQuery())
+                    .setFragment(newUri.getFragment())
+                    .build();
+        } else if (location.contains("../")) {
+            List<String> oldPathList = convertPath(getPath());
+            List<String> newPathList = convertPath(newUri.getPath());
+
+            int start = newPathList.lastIndexOf("..");
+
+            newPathList = newPathList.subList(start + 1, newPathList.size());
+
+            if (!oldPathList.isEmpty()) {
+                oldPathList = oldPathList.subList(0, oldPathList.size() - start - 2);
+                oldPathList.addAll(newPathList);
+                String path = TextUtils.join("/", oldPathList);
+                return builder().setPath(path).setQuery(newUri.getQuery()).setFragment(newUri.getFragment()).build();
+            }
+
+            String path = TextUtils.join("/", newPathList);
+
+            return builder().setPath(path).setQuery(newUri.getQuery()).setFragment(newUri.getFragment()).build();
+        } else {
+            String path = (getPath() + newUri.getPath()).replace("//", "/");
+            return builder().setPath(path).setQuery(newUri.getQuery()).setFragment(newUri.getFragment()).build();
+        }
+    }
+
+    public static class Builder {
+
+        private String mScheme;
+        private String mHost;
+        private int mPort;
+        private String mPath;
+        private Params.Builder mQuery;
+        private String mFragment;
+
+        private Builder(String url) {
+            URI uri = URI.create(url);
+
+            this.mScheme = uri.getScheme();
+            this.mHost = uri.getHost();
+            this.mPort = convertPort(uri.getPort());
+            this.mPath = uri.getPath();
+            this.mQuery = convertQuery(uri.getQuery()).builder();
+            this.mFragment = uri.getFragment();
+        }
+
+        public Builder setScheme(String scheme) {
+            this.mScheme = scheme;
+            return this;
+        }
+
+        public Builder setHost(String host) {
+            this.mHost = host;
+            return this;
+        }
+
+        public Builder setPort(int port) {
+            this.mPort = port;
+            return this;
+        }
+
+        public Builder addPath(int value) {
+            return addPath(Integer.toString(value));
+        }
+
+        public Builder addPath(long value) {
+            return addPath(Long.toString(value));
+        }
+
+        public Builder addPath(boolean value) {
+            return addPath(Boolean.toString(value));
+        }
+
+        public Builder addPath(char value) {
+            return addPath(String.valueOf(value));
+        }
+
+        public Builder addPath(double value) {
+            return addPath(Double.toString(value));
+        }
+
+        public Builder addPath(float value) {
+            return addPath(Float.toString(value));
+        }
+
+        public Builder addPath(String path) {
+            mPath = mPath + "/" + path;
+            return this;
+        }
+
+        public Builder setPath(String path) {
+            mPath = path;
+            return this;
+        }
+
+        public Builder clearPath() {
+            mPath = "";
+            return this;
+        }
+
+        public Builder addQuery(String key, int value) {
+            return addQuery(key, Integer.toString(value));
+        }
+
+        public Builder addQuery(String key, long value) {
+            return addQuery(key, Long.toString(value));
+        }
+
+        public Builder addQuery(String key, boolean value) {
+            return addQuery(key, Boolean.toString(value));
+        }
+
+        public Builder addQuery(String key, char value) {
+            return addQuery(key, String.valueOf(value));
+        }
+
+        public Builder addQuery(String key, double value) {
+            return addQuery(key, Double.toString(value));
+        }
+
+        public Builder addQuery(String key, float value) {
+            return addQuery(key, Float.toString(value));
+        }
+
+        public Builder addQuery(String key, short value) {
+            return addQuery(key, Integer.toString(value));
+        }
+
+        public Builder addQuery(String key, String value) {
+            mQuery.add(key, value);
+            return this;
+        }
+
+        public Builder addQuery(String key, List<String> values) {
+            for (String value : values) {
+                addQuery(key, value);
+            }
+            return this;
+        }
+
+        public Builder addQuery(Params query) {
+            for (Map.Entry<String, List<Object>> entry : query.entrySet()) {
+                String key = entry.getKey();
+                List<Object> values = entry.getValue();
+                for (Object value : values) {
+                    if (value instanceof CharSequence) {
+                        String textValue = value.toString();
+                        addQuery(key, textValue);
+                    }
+                }
+            }
+            return this;
+        }
+
+        public Builder setQuery(String query) {
+            mQuery = convertQuery(query).builder();
+            return this;
+        }
+
+        public Builder setQuery(Params query) {
+            mQuery = query.builder();
+            return this;
+        }
+
+        public Builder removeQuery(String key) {
+            mQuery.remove(key);
+            return this;
+        }
+
+        public Builder clearQuery() {
+            mQuery.clear();
+            return this;
+        }
+
+        public Builder setFragment(String fragment) {
+            this.mFragment = fragment;
+            return this;
+        }
+
+        public Url build() {
+            return new Url(this);
+        }
+    }
+}

+ 214 - 0
kalle/src/main/java/com/yanzhenjie/kalle/UrlBody.java

@@ -0,0 +1,214 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.List;
+
+import static com.yanzhenjie.kalle.Headers.VALUE_APPLICATION_URLENCODED;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/24.
+ */
+public class UrlBody extends BaseContent<StringBody> implements RequestBody {
+
+    private final Params mParams;
+    private final Charset mCharset;
+    private final String mContentType;
+    private UrlBody(Builder builder) {
+        this.mParams = builder.mParams.build();
+        this.mCharset = builder.mCharset == null ? Kalle.getConfig().getCharset() : builder.mCharset;
+        this.mContentType = TextUtils.isEmpty(builder.mContentType) ? VALUE_APPLICATION_URLENCODED : builder.mContentType;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * Copy parameters from url body.
+     */
+    public Params copyParams() {
+        return mParams;
+    }
+
+    @Override
+    public long contentLength() {
+        String body = mParams.toString(true);
+        return IOUtils.toByteArray(body, mCharset).length;
+    }
+
+    @Override
+    public String contentType() {
+        return mContentType + "; charset=" + mCharset.name();
+    }
+
+    @Override
+    protected void onWrite(OutputStream writer) throws IOException {
+        String body = mParams.toString(true);
+        IOUtils.write(writer, body, mCharset);
+    }
+
+    @Override
+    public String toString() {
+        return toString(false);
+    }
+
+    public String toString(boolean encode) {
+        return mParams.toString(encode);
+    }
+
+    public static class Builder {
+
+        private Charset mCharset;
+        private String mContentType;
+        private Params.Builder mParams;
+
+        private Builder() {
+            this.mParams = Params.newBuilder();
+        }
+
+        /**
+         * Data charset.
+         */
+        public Builder charset(Charset charset) {
+            this.mCharset = charset;
+            return this;
+        }
+
+        /**
+         * Content type.
+         */
+        public Builder contentType(String contentType) {
+            this.mContentType = contentType;
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, int value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, long value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, boolean value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, char value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, double value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, float value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, short value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, CharSequence value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameter.
+         */
+        public Builder param(String key, String value) {
+            mParams.add(key, value);
+            return this;
+        }
+
+        /**
+         * Add parameters.
+         */
+        public Builder param(String key, List<String> values) {
+            mParams.add(key, values);
+            return this;
+        }
+
+        /**
+         * Add parameters.
+         */
+        public Builder params(Params params) {
+            mParams.add(params);
+            return this;
+        }
+
+        /**
+         * Remove parameters.
+         */
+        public Builder removeParam(String key) {
+            mParams.remove(key);
+            return this;
+        }
+
+        /**
+         * Clear parameters.
+         */
+        public Builder clearParams() {
+            mParams.clear();
+            return this;
+        }
+
+        public UrlBody build() {
+            return new UrlBody(this);
+        }
+    }
+}

+ 208 - 0
kalle/src/main/java/com/yanzhenjie/kalle/UrlRequest.java

@@ -0,0 +1,208 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import java.util.List;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public class UrlRequest extends Request {
+
+    private final Url mUrl;
+
+    protected UrlRequest(Api api) {
+        super(api);
+        this.mUrl = api.mUrl.build();
+    }
+
+    public static UrlRequest.Builder newBuilder(String url, RequestMethod method) {
+        return newBuilder(Url.newBuilder(url).build(), method);
+    }
+
+    public static UrlRequest.Builder newBuilder(Url url, RequestMethod method) {
+        return new UrlRequest.Builder(url, method);
+    }
+
+    /**
+     * @deprecated use {@link #newBuilder(Url, RequestMethod)} instead.
+     */
+    @Deprecated
+    public static UrlRequest.Builder newBuilder(Url.Builder url, RequestMethod method) {
+        return newBuilder(url.build(), method);
+    }
+
+    @Override
+    public Url url() {
+        return mUrl;
+    }
+
+    @Override
+    public Params copyParams() {
+        return mUrl.getParams();
+    }
+
+    @Override
+    public RequestBody body() {
+        throw new AssertionError("It should not be called.");
+    }
+
+    public static class Api<T extends Api<T>> extends Request.Api<T> {
+
+        private Url.Builder mUrl;
+
+        protected Api(Url url, RequestMethod method) {
+            super(method);
+            this.mUrl = url.builder();
+            this.mUrl.addQuery(Kalle.getConfig().getParams());
+        }
+
+        @Override
+        public T path(int value) {
+            mUrl.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(long value) {
+            mUrl.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(boolean value) {
+            mUrl.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(char value) {
+            mUrl.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(double value) {
+            mUrl.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(float value) {
+            mUrl.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T path(String value) {
+            mUrl.addPath(value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, int value) {
+            mUrl.addQuery(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, long value) {
+            mUrl.addQuery(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, boolean value) {
+            mUrl.addQuery(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, char value) {
+            mUrl.addQuery(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, double value) {
+            mUrl.addQuery(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, float value) {
+            mUrl.addQuery(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, short value) {
+            mUrl.addQuery(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, String value) {
+            mUrl.addQuery(key, value);
+            return (T) this;
+        }
+
+        @Override
+        public T param(String key, List<String> values) {
+            mUrl.addQuery(key, values);
+            return (T) this;
+        }
+
+        /**
+         * Add parameters to url.
+         */
+        public T params(Params params) {
+            mUrl.addQuery(params);
+            return (T) this;
+        }
+
+        /**
+         * Set parameters to url.
+         */
+        public T setParams(Params params) {
+            mUrl.setQuery(params);
+            return (T) this;
+        }
+
+        @Override
+        public T removeParam(String key) {
+            mUrl.removeQuery(key);
+            return (T) this;
+        }
+
+        @Override
+        public T clearParams() {
+            mUrl.clearQuery();
+            return (T) this;
+        }
+    }
+
+    public static class Builder extends UrlRequest.Api<UrlRequest.Builder> {
+
+        private Builder(Url url, RequestMethod method) {
+            super(url, method);
+        }
+
+        public UrlRequest build() {
+            return new UrlRequest(this);
+        }
+    }
+}

+ 34 - 0
kalle/src/main/java/com/yanzhenjie/kalle/XmlBody.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle;
+
+import java.nio.charset.Charset;
+
+import static com.yanzhenjie.kalle.Headers.VALUE_APPLICATION_XML;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public class XmlBody extends StringBody {
+
+    public XmlBody(String body) {
+        this(body, Kalle.getConfig().getCharset());
+    }
+
+    public XmlBody(String body, Charset charset) {
+        super(body, charset, VALUE_APPLICATION_XML);
+    }
+}

+ 73 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/BroadcastNetwork.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiManager;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/20.
+ */
+public class BroadcastNetwork implements Network {
+
+    private final Context mContext;
+    private final NetworkReceiver mReceiver;
+
+    public BroadcastNetwork(Context context) {
+        this.mContext = context.getApplicationContext();
+        this.mReceiver = new NetworkReceiver(new NetworkChecker(mContext));
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
+        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+        filter.addAction("android.net.ethernet.STATE_CHANGE");
+        filter.addAction("android.net.ethernet.ETHERNET_STATE_CHANGED");
+        filter.addAction(Intent.ACTION_SCREEN_OFF);
+        filter.addAction(Intent.ACTION_SCREEN_ON);
+        filter.addAction(Intent.ACTION_USER_PRESENT);
+        mContext.registerReceiver(mReceiver, filter);
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mReceiver.mAvailable;
+    }
+
+    public void destroy() {
+        mContext.unregisterReceiver(mReceiver);
+    }
+
+    private static class NetworkReceiver extends BroadcastReceiver {
+
+        private NetworkChecker mChecker;
+        private boolean mAvailable;
+
+        public NetworkReceiver(NetworkChecker checker) {
+            this.mChecker = checker;
+            this.mAvailable = mChecker.isAvailable();
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mAvailable = mChecker.isAvailable();
+        }
+    }
+}

+ 31 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/ConnectFactory.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect;
+
+import com.yanzhenjie.kalle.Request;
+
+import java.io.IOException;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/20.
+ */
+public interface ConnectFactory {
+    /**
+     * According to the request attribute,
+     * and the server to establish a connection.
+     */
+    Connection connect(Request request) throws IOException;
+}

+ 55 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/Connection.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/20.
+ */
+public interface Connection extends Closeable {
+
+    /**
+     * Gets output stream for socket.
+     */
+    OutputStream getOutputStream() throws IOException;
+
+    /**
+     * Gets response code for server.
+     */
+    int getCode() throws IOException;
+
+    /**
+     * Gets response headers for server.
+     */
+    Map<String, List<String>> getHeaders() throws IOException;
+
+    /**
+     * Gets input stream for socket.
+     */
+    InputStream getInputStream() throws IOException;
+
+    /**
+     * Cancel connect.
+     */
+    void disconnect();
+
+}

+ 41 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/Interceptor.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect;
+
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.connect.http.Chain;
+
+import java.io.IOException;
+
+/**
+ * <p>
+ * Intercept before call request.
+ * </p>
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public interface Interceptor {
+
+    /**
+     * When intercepting the {@link Chain},
+     * here can do some signature and login operation,
+     * it should send a response and return.
+     *
+     * @param chain request chain.
+     * @return the server connection.
+     * @throws IOException io exception may occur during the implementation, you can handle or throw.
+     */
+    Response intercept(Chain chain) throws IOException;
+}

+ 37 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/Network.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public interface Network {
+
+    /**
+     * The network has always been available.
+     */
+    Network DEFAULT = new Network() {
+        @Override
+        public boolean isAvailable() {
+            return true;
+        }
+    };
+
+    /**
+     * Check the network is enable.
+     */
+    boolean isAvailable();
+}

+ 186 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/NetworkChecker.java

@@ -0,0 +1,186 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.telephony.TelephonyManager;
+
+import static com.yanzhenjie.kalle.connect.NetworkChecker.NetType.Mobile;
+import static com.yanzhenjie.kalle.connect.NetworkChecker.NetType.Mobile2G;
+import static com.yanzhenjie.kalle.connect.NetworkChecker.NetType.Mobile3G;
+import static com.yanzhenjie.kalle.connect.NetworkChecker.NetType.Mobile4G;
+import static com.yanzhenjie.kalle.connect.NetworkChecker.NetType.Wifi;
+import static com.yanzhenjie.kalle.connect.NetworkChecker.NetType.Wired;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public class NetworkChecker {
+
+    private ConnectivityManager mManager;
+
+    public NetworkChecker(Context context) {
+        this.mManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+
+    private static boolean isConnected(NetType netType, NetworkInfo networkInfo) {
+        if (networkInfo == null) return false;
+
+        switch (netType) {
+            case Wifi: {
+                if (!isConnected(networkInfo)) return false;
+                return networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+            }
+            case Wired: {
+                if (!isConnected(networkInfo)) return false;
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2)
+                    return networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET;
+                return false;
+            }
+            case Mobile: {
+                if (!isConnected(networkInfo)) return false;
+                return networkInfo.getType() == ConnectivityManager.TYPE_MOBILE;
+            }
+            case Mobile2G: {
+                if (!isConnected(Mobile, networkInfo)) return false;
+                return isMobileSubType(Mobile2G, networkInfo);
+            }
+            case Mobile3G: {
+                if (!isConnected(Mobile, networkInfo)) return false;
+                return isMobileSubType(Mobile3G, networkInfo);
+            }
+            case Mobile4G: {
+                if (!isConnected(Mobile, networkInfo)) return false;
+                return isMobileSubType(Mobile4G, networkInfo);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Whether network connection.
+     */
+    private static boolean isConnected(NetworkInfo networkInfo) {
+        return networkInfo != null && networkInfo.isAvailable() && networkInfo.isConnected();
+    }
+
+    private static boolean isMobileSubType(NetType netType, NetworkInfo networkInfo) {
+        switch (networkInfo.getType()) {
+            case TelephonyManager.NETWORK_TYPE_GSM:
+            case TelephonyManager.NETWORK_TYPE_GPRS:
+            case TelephonyManager.NETWORK_TYPE_CDMA:
+            case TelephonyManager.NETWORK_TYPE_EDGE:
+            case TelephonyManager.NETWORK_TYPE_1xRTT:
+            case TelephonyManager.NETWORK_TYPE_IDEN: {
+                return netType == Mobile2G;
+            }
+            case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
+            case TelephonyManager.NETWORK_TYPE_EVDO_A:
+            case TelephonyManager.NETWORK_TYPE_UMTS:
+            case TelephonyManager.NETWORK_TYPE_EVDO_0:
+            case TelephonyManager.NETWORK_TYPE_HSDPA:
+            case TelephonyManager.NETWORK_TYPE_HSUPA:
+            case TelephonyManager.NETWORK_TYPE_HSPA:
+            case TelephonyManager.NETWORK_TYPE_EVDO_B:
+            case TelephonyManager.NETWORK_TYPE_EHRPD:
+            case TelephonyManager.NETWORK_TYPE_HSPAP: {
+                return netType == Mobile3G;
+            }
+            case TelephonyManager.NETWORK_TYPE_IWLAN:
+            case TelephonyManager.NETWORK_TYPE_LTE: {
+                return netType == Mobile4G;
+            }
+            default: {
+                String subtypeName = networkInfo.getSubtypeName();
+                if (subtypeName.equalsIgnoreCase("TD-SCDMA")
+                        || subtypeName.equalsIgnoreCase("WCDMA")
+                        || subtypeName.equalsIgnoreCase("CDMA2000")) {
+                    return netType == Mobile3G;
+                }
+                break;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Check the network is enable.
+     */
+    public boolean isAvailable() {
+        return isWifiConnected() || isWiredConnected() || isMobileConnected();
+    }
+
+    /**
+     * To determine whether a WiFi network is available.
+     */
+    public final boolean isWifiConnected() {
+        return isAvailable(Wifi);
+    }
+
+    /**
+     * To determine whether a wired network is available.
+     */
+    public final boolean isWiredConnected() {
+        return isAvailable(Wired);
+    }
+
+    /**
+     * Mobile Internet connection.
+     */
+    public final boolean isMobileConnected() {
+        return isAvailable(Mobile);
+    }
+
+    /**
+     * 2G Mobile Internet connection.
+     */
+    public final boolean isMobile2GConnected() {
+        return isAvailable(Mobile2G);
+    }
+
+    /**
+     * 3G Mobile Internet connection.
+     */
+    public final boolean isMobile3GConnected() {
+        return isAvailable(Mobile3G);
+    }
+
+    /**
+     * 4G Mobile Internet connection.
+     */
+    public final boolean isMobile4GConnected() {
+        return isAvailable(Mobile4G);
+    }
+
+    /**
+     * According to the different type of network to determine whether the network connection.
+     */
+    public final boolean isAvailable(NetType netType) {
+        return isConnected(netType, mManager.getActiveNetworkInfo());
+    }
+
+    public enum NetType {
+        Wifi,
+        Wired,
+        Mobile,
+        Mobile2G,
+        Mobile3G,
+        Mobile4G
+    }
+}

+ 37 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/RealTimeNetwork.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2019 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect;
+
+import android.content.Context;
+
+/**
+ * Created by Zhenjie Yan on 2019-05-19.
+ */
+public class RealTimeNetwork implements Network {
+
+    private final Context mContext;
+    private final NetworkChecker mChecker;
+
+    public RealTimeNetwork(Context context) {
+        this.mContext = context.getApplicationContext();
+        this.mChecker = new NetworkChecker(mContext);
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mChecker.isAvailable();
+    }
+}

+ 60 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/StreamBody.java

@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.ResponseBody;
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/22.
+ */
+public class StreamBody implements ResponseBody {
+
+    private String mContentType;
+    private InputStream mStream;
+
+    public StreamBody(String contentType, InputStream stream) {
+        this.mContentType = contentType;
+        this.mStream = stream;
+    }
+
+    @Override
+    public String string() throws IOException {
+        String charset = Headers.parseSubValue(mContentType, "charset", null);
+        return TextUtils.isEmpty(charset) ? IOUtils.toString(mStream) : IOUtils.toString(mStream, charset);
+    }
+
+    @Override
+    public byte[] byteArray() throws IOException {
+        return IOUtils.toByteArray(mStream);
+    }
+
+    @Override
+    public InputStream stream() throws IOException {
+        return mStream;
+    }
+
+    @Override
+    public void close() throws IOException {
+        mStream.close();
+    }
+}

+ 63 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/http/AppChain.java

@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect.http;
+
+import com.yanzhenjie.kalle.Request;
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.connect.Interceptor;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/6.
+ */
+class AppChain implements Chain {
+
+    private final List<Interceptor> mInterceptors;
+    private final int mTargetIndex;
+    private final Request mRequest;
+    private Call mCall;
+
+    AppChain(List<Interceptor> interceptors, int targetIndex, Request request, Call call) {
+        this.mInterceptors = interceptors;
+        this.mTargetIndex = targetIndex;
+        this.mRequest = request;
+        this.mCall = call;
+    }
+
+    @Override
+    public Request request() {
+        return mRequest;
+    }
+
+    @Override
+    public Response proceed(Request request) throws IOException {
+        Interceptor interceptor = mInterceptors.get(mTargetIndex);
+        Chain chain = new AppChain(mInterceptors, mTargetIndex + 1, request, mCall);
+        return interceptor.intercept(chain);
+    }
+
+    @Override
+    public Call newCall() {
+        return mCall;
+    }
+
+    @Override
+    public Call call() {
+        return mCall;
+    }
+}

+ 110 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/http/Call.java

@@ -0,0 +1,110 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect.http;
+
+import com.yanzhenjie.kalle.Kalle;
+import com.yanzhenjie.kalle.Request;
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.connect.Interceptor;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/24.
+ */
+public class Call {
+
+    private static final Executor EXECUTOR = Executors.newCachedThreadPool();
+
+    private final Request mRequest;
+    private ConnectInterceptor mConnectInterceptor;
+    private boolean isExecuted;
+    private boolean isCanceled;
+
+    public Call(Request request) {
+        this.mRequest = request;
+    }
+
+    /**
+     * Execute request.
+     */
+    public Response execute() throws IOException {
+        if (isCanceled) throw new CancellationException("The request has been cancelled.");
+        isExecuted = true;
+
+        List<Interceptor> interceptors = new ArrayList<>(Kalle.getConfig().getInterceptor());
+        mConnectInterceptor = new ConnectInterceptor();
+        interceptors.add(mConnectInterceptor);
+
+        Chain chain = new AppChain(interceptors, 0, mRequest, this);
+        try {
+            return chain.proceed(mRequest);
+        } catch (Exception e) {
+            if (isCanceled) {
+                throw new CancellationException("The request has been cancelled.");
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * The call is executed.
+     *
+     * @return true, otherwise is false.
+     */
+    public boolean isExecuted() {
+        return isExecuted;
+    }
+
+    /**
+     * The call is canceled.
+     *
+     * @return true, otherwise is false.
+     */
+    public boolean isCanceled() {
+        return isCanceled;
+    }
+
+    /**
+     * Cancel the call.
+     */
+    public void cancel() {
+        if (!isCanceled) {
+            isCanceled = true;
+            if (mConnectInterceptor != null) {
+                mConnectInterceptor.cancel();
+            }
+        }
+    }
+
+    /**
+     * Cancel the call asynchronously.
+     */
+    public void asyncCancel() {
+        EXECUTOR.execute(new Runnable() {
+            @Override
+            public void run() {
+                cancel();
+            }
+        });
+    }
+}

+ 57 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/http/Chain.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect.http;
+
+import com.yanzhenjie.kalle.Request;
+import com.yanzhenjie.kalle.Response;
+
+import java.io.IOException;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/6.
+ */
+public interface Chain {
+    /**
+     * Get the request in the chain.
+     *
+     * @return target request.
+     */
+    Request request();
+
+    /**
+     * Proceed to the next request processing.
+     *
+     * @param request target request.
+     * @return {@link Response}.
+     * @throws IOException io exception may occur during the implementation, you can handle or throw.
+     */
+    Response proceed(Request request) throws IOException;
+
+    /**
+     * Return {@link Call}, request will be executed on it.
+     *
+     * @deprecated use {@link #call()} instead.
+     */
+    @Deprecated
+    Call newCall();
+
+    /**
+     * Return {@link Call}, request will be executed on it.
+     *
+     * @return {@link Call}.
+     */
+    Call call();
+}

+ 174 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/http/ConnectInterceptor.java

@@ -0,0 +1,174 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect.http;
+
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.Kalle;
+import com.yanzhenjie.kalle.Request;
+import com.yanzhenjie.kalle.RequestBody;
+import com.yanzhenjie.kalle.RequestMethod;
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.ResponseBody;
+import com.yanzhenjie.kalle.connect.ConnectFactory;
+import com.yanzhenjie.kalle.connect.Connection;
+import com.yanzhenjie.kalle.connect.Interceptor;
+import com.yanzhenjie.kalle.connect.Network;
+import com.yanzhenjie.kalle.connect.StreamBody;
+import com.yanzhenjie.kalle.cookie.CookieManager;
+import com.yanzhenjie.kalle.exception.ConnectException;
+import com.yanzhenjie.kalle.exception.ConnectTimeoutError;
+import com.yanzhenjie.kalle.exception.HostError;
+import com.yanzhenjie.kalle.exception.NetworkError;
+import com.yanzhenjie.kalle.exception.ReadException;
+import com.yanzhenjie.kalle.exception.ReadTimeoutError;
+import com.yanzhenjie.kalle.exception.URLError;
+import com.yanzhenjie.kalle.exception.WriteException;
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CancellationException;
+
+import static com.yanzhenjie.kalle.Headers.KEY_CONTENT_LENGTH;
+import static com.yanzhenjie.kalle.Headers.KEY_CONTENT_TYPE;
+import static com.yanzhenjie.kalle.Headers.KEY_COOKIE;
+import static com.yanzhenjie.kalle.Headers.KEY_HOST;
+import static com.yanzhenjie.kalle.Headers.KEY_SET_COOKIE;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/20.
+ */
+class ConnectInterceptor implements Interceptor {
+
+    private final CookieManager mCookieManager;
+    private final ConnectFactory mFactory;
+    private final Network mNetwork;
+
+    private Connection mConnection;
+    private boolean isCanceled;
+
+    ConnectInterceptor() {
+        this.mCookieManager = new CookieManager(Kalle.getConfig().getCookieStore());
+        this.mFactory = Kalle.getConfig().getConnectFactory();
+        this.mNetwork = Kalle.getConfig().getNetwork();
+    }
+
+    @Override
+    public Response intercept(Chain chain) throws IOException {
+        if (isCanceled) throw new CancellationException("The request has been cancelled.");
+
+        Request request = chain.request();
+        RequestMethod method = request.method();
+
+        if (method.allowBody()) {
+            Headers headers = request.headers();
+            RequestBody body = request.body();
+            headers.set(KEY_CONTENT_LENGTH, Long.toString(body.contentLength()));
+            headers.set(KEY_CONTENT_TYPE, body.contentType());
+            mConnection = connect(request);
+            writeBody(body);
+        } else {
+            mConnection = connect(request);
+        }
+        return readResponse(request);
+    }
+
+    /**
+     * Cancel the request.
+     */
+    public void cancel() {
+        isCanceled = true;
+        if (mConnection != null) {
+            mConnection.disconnect();
+        }
+    }
+
+    /**
+     * Connect to the server to change the connection anomalies occurred.
+     *
+     * @param request target request.
+     * @return connection between client and server.
+     * @throws ConnectException anomalies that occurred during the connection.
+     */
+    private Connection connect(Request request) throws ConnectException {
+        if (!mNetwork.isAvailable())
+            throw new NetworkError(String.format("Network Unavailable: %1$s.", request.url()));
+
+        try {
+            Headers headers = request.headers();
+            URI uri = new URI(request.url().toString());
+            List<String> cookieHeader = mCookieManager.get(uri);
+            if (cookieHeader != null && !cookieHeader.isEmpty())
+                headers.add(KEY_COOKIE, cookieHeader);
+            headers.set(KEY_HOST, uri.getHost());
+            return mFactory.connect(request);
+        } catch (URISyntaxException e) {
+            throw new URLError(String.format("The url syntax error: %1$s.", request.url()), e);
+        } catch (MalformedURLException e) {
+            throw new URLError(String.format("The url is malformed: %1$s.", request.url()), e);
+        } catch (UnknownHostException e) {
+            throw new HostError(String.format("Hostname can not be resolved: %1$s.", request.url()), e);
+        } catch (SocketTimeoutException e) {
+            throw new ConnectTimeoutError(String.format("Connect time out: %1$s.", request.url()), e);
+        } catch (Exception e) {
+            throw new ConnectException(String.format("An unknown exception: %1$s.", request.url()), e);
+        }
+    }
+
+    private void writeBody(RequestBody body) throws WriteException {
+        try {
+            OutputStream stream = mConnection.getOutputStream();
+            body.writeTo(IOUtils.toBufferedOutputStream(stream));
+            IOUtils.closeQuietly(stream);
+        } catch (Exception e) {
+            throw new WriteException(e);
+        }
+    }
+
+    private Response readResponse(Request request) throws ReadException {
+        try {
+            int code = mConnection.getCode();
+            Headers headers = parseResponseHeaders(mConnection.getHeaders());
+
+            List<String> cookieList = headers.get(KEY_SET_COOKIE);
+            if (cookieList != null && !cookieList.isEmpty())
+                mCookieManager.add(URI.create(request.url().toString()), cookieList);
+
+            String contentType = headers.getContentType();
+            ResponseBody body = new StreamBody(contentType, mConnection.getInputStream());
+            return Response.newBuilder().code(code).headers(headers).body(body).build();
+        } catch (SocketTimeoutException e) {
+            throw new ReadTimeoutError(String.format("Read data time out: %1$s.", request.url()), e);
+        } catch (Exception e) {
+            throw new ReadException(e);
+        }
+    }
+
+    private Headers parseResponseHeaders(Map<String, List<String>> headersMap) {
+        Headers headers = new Headers();
+        for (Map.Entry<String, List<String>> entry : headersMap.entrySet()) {
+            headers.add(entry.getKey(), entry.getValue());
+        }
+        return headers;
+    }
+}

+ 87 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/http/LoggerInterceptor.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect.http;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.Request;
+import com.yanzhenjie.kalle.RequestBody;
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.StringBody;
+import com.yanzhenjie.kalle.UrlBody;
+import com.yanzhenjie.kalle.connect.Interceptor;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/26.
+ */
+public class LoggerInterceptor implements Interceptor {
+
+    private final String mTag;
+    private final boolean isEnable;
+
+    public LoggerInterceptor(String tag, boolean isEnable) {
+        this.mTag = tag;
+        this.isEnable = isEnable;
+    }
+
+    @Override
+    public Response intercept(Chain chain) throws IOException {
+        Request request = chain.request();
+        if (isEnable) {
+            Response response = chain.proceed(request);
+
+            String url = request.url().toString();
+
+            StringBuilder log = new StringBuilder(String.format(" \nPrint Request: %1$s.", url));
+            log.append(String.format("\nMethod: %1$s.", request.method().name()));
+
+            Headers toHeaders = request.headers();
+            for (Map.Entry<String, List<String>> entry : toHeaders.entrySet()) {
+                String key = entry.getKey();
+                List<String> values = entry.getValue();
+                log.append(String.format("\n%1$s: %2$s.", key, TextUtils.join(";", values)));
+            }
+
+            if (request.method().allowBody()) {
+                RequestBody body = request.body();
+                if (body instanceof StringBody || body instanceof UrlBody) {
+                    log.append(String.format(" \nRequest Body: %1$s.", body.toString()));
+                }
+            }
+
+            log.append(String.format(" \nPrint Response: %1$s.", url));
+            log.append(String.format(Locale.getDefault(), "\nCode: %1$d", response.code()));
+
+            Headers fromHeaders = response.headers();
+            for (Map.Entry<String, List<String>> entry : fromHeaders.entrySet()) {
+                String key = entry.getKey();
+                List<String> values = entry.getValue();
+                log.append(String.format("\n%1$s: %2$s.", key, TextUtils.join(";", values)));
+            }
+            Log.i(mTag, log.toString());
+            return response;
+        }
+        return chain.proceed(request);
+    }
+
+}

+ 68 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/http/RedirectInterceptor.java

@@ -0,0 +1,68 @@
+/*
+ * Copyright 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect.http;
+
+import com.yanzhenjie.kalle.BodyRequest;
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.Request;
+import com.yanzhenjie.kalle.RequestMethod;
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.Url;
+import com.yanzhenjie.kalle.UrlRequest;
+import com.yanzhenjie.kalle.connect.Interceptor;
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.IOException;
+
+import static com.yanzhenjie.kalle.Headers.KEY_COOKIE;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/6.
+ */
+public class RedirectInterceptor implements Interceptor {
+
+    public RedirectInterceptor() {
+    }
+
+    @Override
+    public Response intercept(Chain chain) throws IOException {
+        Request request = chain.request();
+        Response response = chain.proceed(request);
+        if (response.isRedirect()) {
+            Url oldUrl = request.url();
+            Url url = oldUrl.location(response.headers().getLocation());
+            Headers headers = request.headers();
+            headers.remove(KEY_COOKIE);
+
+            RequestMethod method = request.method();
+            Request newRequest;
+            if (method.allowBody()) {
+                newRequest = BodyRequest.newBuilder(url, method)
+                        .setHeaders(headers)
+                        .setParams(request.copyParams())
+                        .body(request.body())
+                        .build();
+            } else {
+                newRequest = UrlRequest.newBuilder(url, method)
+                        .setHeaders(headers)
+                        .build();
+            }
+            IOUtils.closeQuietly(response);
+            return chain.proceed(newRequest);
+        }
+        return response;
+    }
+}

+ 46 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/http/RetryInterceptor.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect.http;
+
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.connect.Interceptor;
+
+import java.io.IOException;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/6.
+ */
+public class RetryInterceptor implements Interceptor {
+
+    private int mCount;
+
+    public RetryInterceptor(int count) {
+        this.mCount = count;
+    }
+
+    @Override
+    public Response intercept(Chain chain) throws IOException {
+        try {
+            return chain.proceed(chain.request());
+        } catch (IOException e) {
+            if (mCount > 0) {
+                mCount--;
+                return intercept(chain);
+            }
+            throw e;
+        }
+    }
+}

+ 76 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/stream/NullStream.java

@@ -0,0 +1,76 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect.stream;
+
+import com.yanzhenjie.kalle.connect.Connection;
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.InputStream;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/25.
+ */
+public class NullStream extends InputStream {
+
+    private final Connection mConnection;
+
+    public NullStream(Connection connection) {
+        this.mConnection = connection;
+    }
+
+    @Override
+    public int read() {
+        return 0;
+    }
+
+    @Override
+    public int read(byte[] b) {
+        return 0;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) {
+        return 0;
+    }
+
+    @Override
+    public void close() {
+        IOUtils.closeQuietly(mConnection);
+    }
+
+    @Override
+    public long skip(long n) {
+        return 0;
+    }
+
+    @Override
+    public int available() {
+        return 0;
+    }
+
+    @Override
+    public void reset() {
+    }
+
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    @Override
+    public void mark(int limit) {
+    }
+}

+ 82 - 0
kalle/src/main/java/com/yanzhenjie/kalle/connect/stream/SourceStream.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.connect.stream;
+
+import com.yanzhenjie.kalle.connect.Connection;
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/25.
+ */
+public class SourceStream extends InputStream {
+
+    private final Connection mConnection;
+    private final InputStream mStream;
+
+    public SourceStream(Connection connection, InputStream stream) {
+        this.mConnection = connection;
+        this.mStream = stream;
+    }
+
+    @Override
+    public int read() throws IOException {
+        return mStream.read();
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        return mStream.read(b, 0, b.length);
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        return mStream.read(b, off, len);
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        return mStream.skip(n);
+    }
+
+    @Override
+    public int available() throws IOException {
+        return mStream.available();
+    }
+
+    @Override
+    public void close() throws IOException {
+        IOUtils.closeQuietly(mStream);
+        IOUtils.closeQuietly(mConnection);
+    }
+
+    @Override
+    public void reset() throws IOException {
+        mStream.reset();
+    }
+
+    @Override
+    public synchronized void mark(int limit) {
+        mStream.mark(limit);
+    }
+
+    @Override
+    public boolean markSupported() {
+        return mStream.markSupported();
+    }
+}

+ 208 - 0
kalle/src/main/java/com/yanzhenjie/kalle/cookie/Cookie.java

@@ -0,0 +1,208 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.cookie;
+
+import android.text.TextUtils;
+
+import java.io.Serializable;
+import java.net.HttpCookie;
+
+/**
+ * <p>Cookie entity.</p>
+ * Created in Dec 17, 2015 7:21:16 PM.
+ */
+public class Cookie implements Serializable {
+
+    private long id = -1;
+    private String url;
+    private String name;
+    private String value;
+    private String comment;
+    private String commentURL;
+    private boolean discard;
+    private String domain;
+    private long expiry;
+    private String path;
+    private String portList;
+    private boolean secure;
+    private int version = 1;
+
+    public Cookie() {
+    }
+
+    public static Cookie toCookie(String url, HttpCookie httpCookie) {
+        Cookie cookie = new Cookie();
+        cookie.setUrl(url);
+        cookie.setName(httpCookie.getName());
+        cookie.setValue(httpCookie.getValue());
+        cookie.setComment(httpCookie.getComment());
+        cookie.setCommentURL(httpCookie.getCommentURL());
+        cookie.setDiscard(httpCookie.getDiscard());
+        cookie.setDomain(httpCookie.getDomain());
+        long maxAge = httpCookie.getMaxAge();
+        if (maxAge > 0) {
+            long expiry = (maxAge * 1000L) + System.currentTimeMillis();
+            if (expiry < 0L) {
+                expiry = System.currentTimeMillis() + 100L * 365L * 24L * 60L * 60L * 1000L;
+            }
+            cookie.setExpiry(expiry);
+        } else if (maxAge < 0) {
+            cookie.setExpiry(-1);
+        } else {
+            cookie.setExpiry(0);
+        }
+
+        String path = httpCookie.getPath();
+        if (!TextUtils.isEmpty(path) && path.length() > 1 && path.endsWith("/")) {
+            path = path.substring(0, path.length() - 1);
+        }
+        cookie.setPath(path);
+        cookie.setPortList(httpCookie.getPortlist());
+        cookie.setSecure(httpCookie.getSecure());
+        cookie.setVersion(httpCookie.getVersion());
+        return cookie;
+    }
+
+    public static HttpCookie toHttpCookie(Cookie cookie) {
+        HttpCookie httpCookie = new HttpCookie(cookie.name, cookie.value);
+        httpCookie.setComment(cookie.comment);
+        httpCookie.setCommentURL(cookie.commentURL);
+        httpCookie.setDiscard(cookie.discard);
+        httpCookie.setDomain(cookie.domain);
+        if (cookie.expiry == 0) {
+            httpCookie.setMaxAge(0);
+        } else if (cookie.expiry < 0) {
+            httpCookie.setMaxAge(-1L);
+        } else {
+            long expiry = cookie.expiry - System.currentTimeMillis();
+            expiry = expiry <= 0 ? 0 : expiry;
+            httpCookie.setMaxAge(expiry / 1000L);
+        }
+        httpCookie.setPath(cookie.path);
+        httpCookie.setPortlist(cookie.portList);
+        httpCookie.setSecure(cookie.secure);
+        httpCookie.setVersion(cookie.version);
+        return httpCookie;
+    }
+
+    public static boolean isExpired(Cookie entity) {
+        return entity.expiry != -1L && entity.expiry < System.currentTimeMillis();
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public String getComment() {
+        return comment;
+    }
+
+    public void setComment(String comment) {
+        this.comment = comment;
+    }
+
+    public String getCommentURL() {
+        return commentURL;
+    }
+
+    public void setCommentURL(String commentURL) {
+        this.commentURL = commentURL;
+    }
+
+    public boolean isDiscard() {
+        return discard;
+    }
+
+    public void setDiscard(boolean discard) {
+        this.discard = discard;
+    }
+
+    public String getDomain() {
+        return domain;
+    }
+
+    public void setDomain(String domain) {
+        this.domain = domain;
+    }
+
+    public long getExpiry() {
+        return expiry;
+    }
+
+    public void setExpiry(long expiry) {
+        this.expiry = expiry;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    public String getPortList() {
+        return portList;
+    }
+
+    public void setPortList(String portList) {
+        this.portList = portList;
+    }
+
+    public boolean isSecure() {
+        return secure;
+    }
+
+    public void setSecure(boolean secure) {
+        this.secure = secure;
+    }
+
+    public int getVersion() {
+        return version;
+    }
+
+    public void setVersion(int version) {
+        this.version = version;
+    }
+}

+ 168 - 0
kalle/src/main/java/com/yanzhenjie/kalle/cookie/CookieManager.java

@@ -0,0 +1,168 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.cookie;
+
+import android.text.TextUtils;
+
+import java.net.HttpCookie;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/22.
+ */
+public class CookieManager {
+
+    private CookieStore cookieJar;
+
+    public CookieManager(CookieStore store) {
+        this.cookieJar = store;
+    }
+
+    private static int getPort(URI uri) {
+        int port = uri.getPort();
+        return (port == -1) ? ("https".equals(uri.getScheme()) ? 443 : 80) : port;
+    }
+
+    private static boolean containsPort(String portList, int port) {
+        if (portList.contains(",")) {
+            String[] portArray = portList.split(",");
+            String inPort = Integer.toString(port);
+            for (String outPort : portArray) {
+                if (outPort.equals(inPort)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        return portList.equalsIgnoreCase(Integer.toString(port));
+    }
+
+    private static boolean pathMatches(URI uri, HttpCookie cookie) {
+        return normalizePath(uri.getPath()).startsWith(normalizePath(cookie.getPath()));
+    }
+
+    private static String normalizePath(String path) {
+        if (path == null) {
+            path = "";
+        }
+        if (!path.endsWith("/")) {
+            path = path + "/";
+        }
+        return path;
+    }
+
+    /**
+     * Get the cookie under the specified URI, where https and secure will be verified.
+     *
+     * @param uri uri.
+     * @return all cookies that match the rules.
+     */
+    public List<String> get(URI uri) {
+        boolean secureLink = "https".equalsIgnoreCase(uri.getScheme());
+        List<HttpCookie> outCookieList = new ArrayList<>();
+        List<HttpCookie> inCookieList = cookieJar.get(uri);
+        for (HttpCookie cookie : inCookieList) {
+            if (pathMatches(uri, cookie) && (secureLink || !cookie.getSecure())) {
+                String portList = cookie.getPortlist();
+                int port = getPort(uri);
+                if (TextUtils.isEmpty(portList) || containsPort(portList, port)) {
+                    outCookieList.add(cookie);
+                }
+            }
+        }
+        if (outCookieList.isEmpty()) return Collections.emptyList();
+
+        List<String> cookieList = new ArrayList<>();
+        cookieList.add(sortByPath(outCookieList));
+        return cookieList;
+    }
+
+    /**
+     * Cookie for the specified URI to save, where path and port will be verified.
+     *
+     * @param uri        uri.
+     * @param cookieList all you want to save the Cookie, does not meet the rules will not be saved.
+     */
+    public void add(URI uri, List<String> cookieList) {
+        for (String cookieValue : cookieList) {
+            List<HttpCookie> cookies = HttpCookie.parse(cookieValue);
+            for (HttpCookie cookie : cookies) {
+                if (cookie.getPath() == null) {
+                    String path = normalizePath(uri.getPath());
+                    cookie.setPath(path);
+                } else if (!pathMatches(uri, cookie)) {
+                    continue;
+                }
+
+                if (cookie.getDomain() == null) cookie.setDomain(uri.getHost());
+
+                String portList = cookie.getPortlist();
+                int port = getPort(uri);
+                if (TextUtils.isEmpty(portList) || containsPort(portList, port)) {
+                    cookieJar.add(uri, cookie);
+                }
+            }
+        }
+    }
+
+    private String sortByPath(List<HttpCookie> cookies) {
+        Collections.sort(cookies, new CookiePathComparator());
+        final StringBuilder result = new StringBuilder();
+        int minVersion = 1;
+        for (HttpCookie cookie : cookies) {
+            if (cookie.getVersion() < minVersion) {
+                minVersion = cookie.getVersion();
+            }
+        }
+
+        if (minVersion == 1) {
+            result.append("$Version=\"1\"; ");
+        }
+
+        for (int i = 0; i < cookies.size(); ++i) {
+            if (i != 0) {
+                result.append("; ");
+            }
+
+            result.append(cookies.get(i).toString());
+        }
+
+        return result.toString();
+    }
+
+
+    private static class CookiePathComparator implements Comparator<HttpCookie> {
+        @Override
+        public int compare(HttpCookie c1, HttpCookie c2) {
+            if (c1 == c2) return 0;
+            if (c1 == null) return -1;
+            if (c2 == null) return 1;
+
+            if (!c1.getName().equals(c2.getName())) return 0;
+
+            final String c1Path = normalizePath(c1.getPath());
+            final String c2Path = normalizePath(c2.getPath());
+
+            if (c1Path.startsWith(c2Path)) return -1;
+            else if (c2Path.startsWith(c1Path)) return 1;
+            else return 0;
+        }
+    }
+}

+ 74 - 0
kalle/src/main/java/com/yanzhenjie/kalle/cookie/CookieStore.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.cookie;
+
+import java.net.HttpCookie;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/20.
+ */
+public interface CookieStore {
+
+    CookieStore DEFAULT = new CookieStore() {
+        @Override
+        public List<HttpCookie> get(URI uri) {
+            return Collections.emptyList();
+        }
+
+        @Override
+        public void add(URI uri, HttpCookie httpCookie) {
+        }
+
+        @Override
+        public void remove(HttpCookie httpCookie) {
+        }
+
+        @Override
+        public void clear() {
+        }
+    };
+
+    /**
+     * According to url loading cookies.
+     *
+     * @param uri uri.
+     * @return all cookies that match the rules.
+     */
+    List<HttpCookie> get(URI uri);
+
+    /**
+     * Save cookie for the specified url.
+     *
+     * @param uri    uri.
+     * @param cookie cookie.
+     */
+    void add(URI uri, HttpCookie cookie);
+
+    /**
+     * Remove the specified cookie.
+     *
+     * @param cookie cookie.
+     */
+    void remove(HttpCookie cookie);
+
+    /**
+     * Clear the cookie.
+     */
+    void clear();
+}

+ 195 - 0
kalle/src/main/java/com/yanzhenjie/kalle/cookie/DBCookieStore.java

@@ -0,0 +1,195 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.cookie;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.cookie.db.CookieDao;
+import com.yanzhenjie.kalle.cookie.db.Field;
+import com.yanzhenjie.kalle.cookie.db.Where;
+
+import java.net.HttpCookie;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Created in Dec 17, 2015 7:20:52 PM.
+ */
+public class DBCookieStore implements CookieStore, Field {
+
+    private final static int MAX_COOKIE_SIZE = 888;
+    private Lock mLock;
+    private CookieDao mCookieDao;
+    private DBCookieStore(Builder builder) {
+        mLock = new ReentrantLock();
+        mCookieDao = new CookieDao(builder.mContext);
+
+        Where where = Where.newBuilder()
+                .add(EXPIRY, Where.Options.EQUAL, -1)
+                .or(EXPIRY, Where.Options.EQUAL, 0)
+                .build();
+        mCookieDao.delete(where.toString());
+    }
+
+    public static Builder newBuilder(Context context) {
+        return new Builder(context);
+    }
+
+    private static URI getEffectiveURI(final URI uri) {
+        URI effectiveURI;
+        try {
+            effectiveURI = new URI("http", uri.getHost(), uri.getPath(), null, null);
+        } catch (URISyntaxException e) {
+            effectiveURI = uri;
+        }
+        return effectiveURI;
+    }
+
+    @Override
+    public List<HttpCookie> get(URI uri) {
+        mLock.lock();
+        try {
+            uri = getEffectiveURI(uri);
+
+            Where.Builder whereBuilder = Where.newBuilder();
+
+            String host = uri.getHost();
+            if (!TextUtils.isEmpty(host)) {
+                Where.Builder subBuilder = Where.newBuilder()
+                        .add(DOMAIN, Where.Options.EQUAL, host)
+                        .or(DOMAIN, Where.Options.EQUAL, "." + host);
+
+                int firstDot = host.indexOf(".");
+                int lastDot = host.lastIndexOf(".");
+                if (firstDot > 0) {
+                    if (lastDot > firstDot) {
+                        String domain = host.substring(firstDot, host.length());
+                        if (!TextUtils.isEmpty(domain)) {
+                            subBuilder.or(DOMAIN, Where.Options.EQUAL, domain);
+                        }
+                    }
+                    if (lastDot > firstDot + 1) {
+                        String domain = host.substring(firstDot + 1, host.length());
+                        if (!TextUtils.isEmpty(domain)) {
+                            subBuilder.or(DOMAIN, Where.Options.EQUAL, domain);
+                        }
+                    }
+                }
+                whereBuilder.set(subBuilder.build().toString());
+            }
+
+            String path = uri.getPath();
+            if (!TextUtils.isEmpty(path)) {
+                Where.Builder subBuilder = Where.newBuilder()
+                        .add(PATH, Where.Options.EQUAL, path)
+                        .or(PATH, Where.Options.EQUAL, "/")
+                        .orNull(PATH);
+                int lastSplit = path.lastIndexOf("/");
+                while (lastSplit > 0) {
+                    path = path.substring(0, lastSplit);
+                    subBuilder.or(PATH, Where.Options.EQUAL, path);
+                    lastSplit = path.lastIndexOf("/");
+                }
+                subBuilder.bracket();
+                whereBuilder.and(subBuilder.build());
+            }
+
+            whereBuilder.or(URL, Where.Options.EQUAL, uri.toString());
+
+            Where where = whereBuilder.build();
+            List<Cookie> cookieList = mCookieDao.getList(where.toString(), null, null, null);
+            List<HttpCookie> returnedCookies = new ArrayList<>();
+            for (Cookie cookie : cookieList) {
+                if (!Cookie.isExpired(cookie)) returnedCookies.add(Cookie.toHttpCookie(cookie));
+            }
+            return returnedCookies;
+        } finally {
+            mLock.unlock();
+        }
+    }
+
+    @Override
+    public void add(URI uri, HttpCookie httpCookie) {
+        mLock.lock();
+        try {
+            if (uri != null && httpCookie != null) {
+                uri = getEffectiveURI(uri);
+                mCookieDao.replace(Cookie.toCookie(uri.toString(), httpCookie));
+                trimSize();
+            }
+        } finally {
+            mLock.unlock();
+        }
+    }
+
+    @Override
+    public void remove(HttpCookie httpCookie) {
+        mLock.lock();
+        try {
+            Where.Builder whereBuilder = Where.newBuilder().add(NAME, Where.Options.EQUAL, httpCookie.getName());
+
+            String domain = httpCookie.getDomain();
+            if (!TextUtils.isEmpty(domain)) whereBuilder.and(DOMAIN, Where.Options.EQUAL, domain);
+
+            String path = httpCookie.getPath();
+            if (!TextUtils.isEmpty(path)) {
+                if (path.length() > 1 && path.endsWith("/")) {
+                    path = path.substring(0, path.length() - 1);
+                }
+                whereBuilder.and(PATH, Where.Options.EQUAL, path);
+            }
+            mCookieDao.delete(whereBuilder.build().toString());
+        } finally {
+            mLock.unlock();
+        }
+    }
+
+    @Override
+    public void clear() {
+        mLock.lock();
+        try {
+            mCookieDao.deleteAll();
+        } finally {
+            mLock.unlock();
+        }
+    }
+
+    private void trimSize() {
+        int count = mCookieDao.count();
+        if (count > MAX_COOKIE_SIZE) {
+            List<Cookie> rmList = mCookieDao.getList(null, null, Integer.toString(count - MAX_COOKIE_SIZE), null);
+            if (rmList != null) mCookieDao.delete(rmList);
+        }
+    }
+
+    public static class Builder {
+
+        private Context mContext;
+
+        private Builder(Context context) {
+            this.mContext = context;
+        }
+
+        public DBCookieStore build() {
+            return new DBCookieStore(this);
+        }
+    }
+}

+ 207 - 0
kalle/src/main/java/com/yanzhenjie/kalle/cookie/db/CookieDao.java

@@ -0,0 +1,207 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.cookie.db;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.cookie.Cookie;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created in Jan 10, 2016 8:18:28 PM.
+ */
+public class CookieDao implements Field {
+
+    private SQLHelper mSQLHelper;
+
+    public CookieDao(Context context) {
+        this.mSQLHelper = new SQLHelper(context);
+    }
+
+    protected final SQLiteDatabase getDateBase() {
+        return mSQLHelper.getReadableDatabase();
+    }
+
+    protected final void closeDateBase(SQLiteDatabase database) {
+        if (database != null && database.isOpen())
+            database.close();
+    }
+
+    protected final void closeCursor(Cursor cursor) {
+        if (cursor != null && !cursor.isClosed()) {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Query the number of records.
+     */
+    public int count() {
+        return count("SELECT COUNT(" + ID + ") FROM " + TABLE_NAME);
+    }
+
+    /**
+     * Query the number of records.
+     */
+    public int count(String sql) {
+        SQLiteDatabase database = getDateBase();
+        Cursor cursor = database.rawQuery(sql, null);
+        try {
+            return cursor.moveToNext() ? cursor.getInt(0) : 0;
+        } finally {
+            closeCursor(cursor);
+            closeDateBase(database);
+        }
+    }
+
+    /**
+     * Delete all cookie.
+     */
+    public boolean deleteAll() {
+        return delete("1=1");
+    }
+
+    /**
+     * Delete the cookie in the specified list.
+     */
+    public boolean delete(List<Cookie> cookies) {
+        List<Long> idList = new ArrayList<>();
+        for (Cookie cookie : cookies) {
+            idList.add(cookie.getId());
+        }
+        Where where = Where.newBuilder().in(ID, idList).build();
+        return delete(where.toString());
+    }
+
+    /**
+     * Delete cookies based on the conditions.
+     */
+    public boolean delete(String where) {
+        SQLiteDatabase database = getDateBase();
+        String sql = "DELETE FROM " + TABLE_NAME + " WHERE " + where;
+        database.beginTransaction();
+        try {
+            database.execSQL(sql);
+            database.setTransactionSuccessful();
+            return true;
+        } catch (SQLException e) {
+            return false;
+        } finally {
+            database.endTransaction();
+            closeDateBase(database);
+        }
+    }
+
+    /**
+     * Query all cookie.
+     */
+    public List<Cookie> getAll() {
+        return getList(null, null, null, null);
+    }
+
+    /**
+     * Query the cookie list based on the conditions.
+     */
+    public List<Cookie> getList(String where, String orderBy, String limit, String offset) {
+        StringBuilder sqlBuild = new StringBuilder("SELECT ").append("*").append(" FROM ").append(TABLE_NAME);
+        if (!TextUtils.isEmpty(where)) {
+            sqlBuild.append(" WHERE ");
+            sqlBuild.append(where);
+        }
+        if (!TextUtils.isEmpty(orderBy)) {
+            sqlBuild.append(" ORDER BY ");
+            sqlBuild.append(orderBy);
+        }
+        if (!TextUtils.isEmpty(limit)) {
+            sqlBuild.append(" LIMIT ");
+            sqlBuild.append(limit);
+
+            if (!TextUtils.isEmpty(offset)) {
+                sqlBuild.append(" OFFSET ");
+                sqlBuild.append(offset);
+            }
+        }
+        return getList(sqlBuild.toString());
+    }
+
+    /**
+     * Save or set cookies.
+     */
+    public long replace(Cookie cookie) {
+        SQLiteDatabase database = getDateBase();
+        database.beginTransaction();
+
+        ContentValues values = new ContentValues();
+        values.put(URL, cookie.getUrl());
+        values.put(NAME, cookie.getName());
+        values.put(VALUE, cookie.getValue());
+        values.put(COMMENT, cookie.getComment());
+        values.put(COMMENT_URL, cookie.getCommentURL());
+        values.put(DISCARD, String.valueOf(cookie.isDiscard()));
+        values.put(DOMAIN, cookie.getDomain());
+        values.put(EXPIRY, cookie.getExpiry());
+        values.put(PATH, cookie.getPath());
+        values.put(PORT_LIST, cookie.getPortList());
+        values.put(SECURE, String.valueOf(cookie.isSecure()));
+        values.put(VERSION, cookie.getVersion());
+        try {
+            long result = database.replace(TABLE_NAME, null, values);
+            database.setTransactionSuccessful();
+            return result;
+        } catch (Exception e) {
+            return -1;
+        } finally {
+            database.endTransaction();
+            closeDateBase(database);
+        }
+    }
+
+    /**
+     * According to the unique index adds or updates a row data.
+     */
+    public List<Cookie> getList(String querySql) {
+        SQLiteDatabase database = getDateBase();
+        List<Cookie> cookieList = new ArrayList<>();
+        Cursor cursor = database.rawQuery(querySql, null);
+        while (cursor.moveToNext()) {
+            Cookie cookie = new Cookie();
+            cookie.setId(cursor.getInt(cursor.getColumnIndex(ID)));
+            cookie.setUrl(cursor.getString(cursor.getColumnIndex(URL)));
+            cookie.setName(cursor.getString(cursor.getColumnIndex(NAME)));
+            cookie.setValue(cursor.getString(cursor.getColumnIndex(VALUE)));
+            cookie.setComment(cursor.getString(cursor.getColumnIndex(COMMENT)));
+            cookie.setCommentURL(cursor.getString(cursor.getColumnIndex(COMMENT_URL)));
+            cookie.setDiscard("true".equals(cursor.getString(cursor.getColumnIndex(DISCARD))));
+            cookie.setDomain(cursor.getString(cursor.getColumnIndex(DOMAIN)));
+            cookie.setExpiry(cursor.getLong(cursor.getColumnIndex(EXPIRY)));
+            cookie.setPath(cursor.getString(cursor.getColumnIndex(PATH)));
+            cookie.setPortList(cursor.getString(cursor.getColumnIndex(PORT_LIST)));
+            cookie.setSecure("true".equals(cursor.getString(cursor.getColumnIndex(SECURE))));
+            cookie.setVersion(cursor.getInt(cursor.getColumnIndex(VERSION)));
+            cookieList.add(cookie);
+        }
+        closeCursor(cursor);
+        closeDateBase(database);
+        return cookieList;
+    }
+}

+ 37 - 0
kalle/src/main/java/com/yanzhenjie/kalle/cookie/db/Field.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.cookie.db;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/20.
+ */
+public interface Field {
+    String TABLE_NAME = "COOKIES_TABLE";
+
+    String ID = "_ID";
+    String URL = "URL";
+    String NAME = "NAME";
+    String VALUE = "VALUE";
+    String COMMENT = "COMMENT";
+    String COMMENT_URL = "COMMENT_URL";
+    String DISCARD = "DISCARD";
+    String DOMAIN = "DOMAIN";
+    String EXPIRY = "EXPIRY";
+    String PATH = "PATH";
+    String PORT_LIST = "PORT_LIST";
+    String SECURE = "SECURE";
+    String VERSION = "VERSION";
+}

+ 67 - 0
kalle/src/main/java/com/yanzhenjie/kalle/cookie/db/SQLHelper.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.cookie.db;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/**
+ * <p>Cookie database operation class.</p>
+ * Created in Dec 18, 2015 6:30:59 PM.
+ */
+final class SQLHelper extends SQLiteOpenHelper implements Field {
+
+    private static final String DB_COOKIE_NAME = "_kalle_cookies_db.db";
+    private static final int DB_COOKIE_VERSION = 3;
+
+    private static final String SQL_CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+            " " + URL + " TEXT, " + NAME + " TEXT, " + VALUE + " TEXT, " + COMMENT + " TEXT, " + COMMENT_URL + " TEXT, " + DISCARD + " TEXT," +
+            " " + DOMAIN + " TEXT, " + EXPIRY + " INTEGER, " + PATH + " TEXT, " + PORT_LIST + " TEXT, " + SECURE + " TEXT, " + VERSION + " INTEGER)";
+    private static final String SQL_CREATE_UNIQUE_INDEX = "CREATE UNIQUE INDEX COOKIE_UNIQUE_INDEX ON COOKIES_TABLE(\"" + NAME + "\", \"" + DOMAIN + "\", \"" + PATH + "\")";
+    private static final String SQL_DELETE_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME;
+
+    SQLHelper(Context context) {
+        super(context.getApplicationContext(), DB_COOKIE_NAME, null, DB_COOKIE_VERSION);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.beginTransaction();
+        try {
+            db.execSQL(SQL_CREATE_TABLE);
+            db.execSQL(SQL_CREATE_UNIQUE_INDEX);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        if (newVersion != oldVersion) {
+            db.beginTransaction();
+            try {
+                db.execSQL(SQL_DELETE_TABLE);
+                db.execSQL(SQL_CREATE_TABLE);
+                db.execSQL(SQL_CREATE_UNIQUE_INDEX);
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
+        }
+    }
+}

+ 146 - 0
kalle/src/main/java/com/yanzhenjie/kalle/cookie/db/Where.java

@@ -0,0 +1,146 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.cookie.db;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Created in Dec 19, 2015 4:16:24 PM.
+ */
+public class Where {
+
+    private StringBuilder mBuilder;
+
+    private Where(Builder builder) {
+        this.mBuilder = builder.mBuilder;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    @Override
+    public String toString() {
+        return mBuilder.toString();
+    }
+
+    public enum Options {
+
+        EQUAL(" = "), NO_EQUAL(" != "), BIGGER(" > "), SMALLER(" < ");
+
+        private String value;
+
+        Options(String value) {
+            this.value = value;
+        }
+    }
+
+    public static class Builder {
+        private StringBuilder mBuilder;
+
+        private Builder() {
+            mBuilder = new StringBuilder();
+        }
+
+        public Builder append(Object row) {
+            mBuilder.append(row);
+            return this;
+        }
+
+        public Builder set(String row) {
+            mBuilder.delete(0, mBuilder.length()).append(row);
+            return this;
+        }
+
+        public Builder isNull(CharSequence columnName) {
+            mBuilder.append("\"").append(columnName).append("\"").append(" IS ").append("NULL");
+            return this;
+        }
+
+        private Builder add(CharSequence columnName, Options op) {
+            mBuilder.append("\"").append(columnName).append("\"").append(op.value);
+            return this;
+        }
+
+        public Builder add(CharSequence columnName, Options op, Object value) {
+            add(columnName, op).append("'").append(value).append("'");
+            return this;
+        }
+
+        public <T> Builder in(CharSequence columnName, List<T> values) {
+            mBuilder.append(columnName).append(" IN ").append("(");
+            StringBuilder sb = new StringBuilder();
+            Iterator<?> it = values.iterator();
+            if (it.hasNext()) {
+                sb.append("'").append(it.next()).append("'");
+                while (it.hasNext()) {
+                    sb.append(", '").append(it.next()).append("'");
+                }
+            }
+            mBuilder.append(sb).append(")");
+            return this;
+        }
+
+        public Builder and() {
+            if (mBuilder.length() > 0) mBuilder.append(" AND ");
+            return this;
+        }
+
+        public Builder and(CharSequence columnName, Options op, Object value) {
+            return and().add(columnName, op, value);
+        }
+
+        public Builder andNull(CharSequence columnName) {
+            return and().isNull(columnName);
+        }
+
+        public Builder and(Where where) {
+            return and().append(where);
+        }
+
+        public Builder or() {
+            if (mBuilder.length() > 0)
+                mBuilder.append(" OR ");
+            return this;
+        }
+
+        public Builder or(CharSequence columnName, Options op, Object value) {
+            return or().add(columnName, op, value);
+        }
+
+        public Builder orNull(CharSequence columnName) {
+            return or().isNull(columnName);
+        }
+
+        public Builder or(Where where) {
+            return or().append(where);
+        }
+
+        public Builder bracket() {
+            return insert(0, "(").append(')');
+        }
+
+        public Builder insert(int index, CharSequence s) {
+            mBuilder.insert(index, s);
+            return this;
+        }
+
+        public Where build() {
+            return new Where(this);
+        }
+    }
+}

+ 234 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/BasicWorker.java

@@ -0,0 +1,234 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.Kalle;
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.Url;
+import com.yanzhenjie.kalle.exception.DownloadError;
+import com.yanzhenjie.kalle.util.IOUtils;
+import com.yanzhenjie.kalle.util.UrlUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+
+import static com.yanzhenjie.kalle.Headers.KEY_RANGE;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public abstract class BasicWorker<T extends Download> implements Callable<String> {
+
+    private final T mDownload;
+
+    private String mDirectory;
+    private String mFileName;
+    private Download.ProgressBar mProgressBar;
+    private Download.Policy mPolicy;
+
+    BasicWorker(T download) {
+        this.mDownload = download;
+        this.mDirectory = mDownload.directory();
+        this.mFileName = mDownload.fileName();
+        this.mProgressBar = new AsyncProgressBar(mDownload.progressBar());
+        this.mPolicy = mDownload.policy();
+    }
+
+    @Override
+    public String call() throws Exception {
+        if (TextUtils.isEmpty(mDirectory)) throw new IOException("Please specify the directory.");
+        File directory = new File(mDirectory);
+        IOUtils.createFolder(directory);
+
+        Response response = null;
+        try {
+            int code;
+            Headers comeHeaders;
+            File tempFile;
+
+            if (TextUtils.isEmpty(mFileName)) {
+                response = requestNetwork(mDownload);
+                code = response.code();
+                comeHeaders = response.headers();
+                mFileName = getRealFileName(comeHeaders);
+                tempFile = new File(mDirectory, mFileName + ".kalle");
+            } else {
+                tempFile = new File(mDirectory, mFileName + ".kalle");
+                if (mPolicy.isRange() && tempFile.exists()) {
+                    Headers toHeaders = mDownload.headers();
+                    toHeaders.set(KEY_RANGE, "bytes=" + tempFile.length() + "-");
+                    response = requestNetwork(mDownload);
+                    code = response.code();
+                    comeHeaders = response.headers();
+                } else {
+                    response = requestNetwork(mDownload);
+                    code = response.code();
+                    comeHeaders = response.headers();
+
+                    IOUtils.delFileOrFolder(tempFile);
+                }
+            }
+
+            if (!mPolicy.allowDownload(code, comeHeaders)) {
+                throw new DownloadError(code, comeHeaders, "The download policy prohibits the program from continuing to download.");
+            }
+
+            File file = new File(mDirectory, mFileName);
+            if (file.exists()) {
+                String filePath = file.getAbsolutePath();
+                if (mPolicy.oldAvailable(filePath, code, comeHeaders)) {
+                    mProgressBar.onProgress(100, file.length(), 0);
+                    return filePath;
+                } else {
+                    IOUtils.delFileOrFolder(file);
+                }
+            }
+
+            long contentLength;
+
+            if (code == 206) {
+                String range = comeHeaders.getContentRange();
+                contentLength = Long.parseLong(range.substring(range.indexOf('/') + 1));
+            } else {
+                IOUtils.createNewFile(tempFile);
+                contentLength = comeHeaders.getContentLength();
+            }
+
+            long oldCount = tempFile.length();
+            int oldProgress = 0;
+            long oldSpeed = 0;
+
+            RandomAccessFile randomFile = new RandomAccessFile(tempFile, "rws");
+            randomFile.seek(oldCount);
+
+            int len;
+            byte[] buffer = new byte[8096];
+
+            long speedTime = System.currentTimeMillis();
+            long speedCount = 0;
+
+            InputStream stream = response.body().stream();
+
+            while (((len = stream.read(buffer)) != -1)) {
+                randomFile.write(buffer, 0, len);
+
+                oldCount += len;
+                speedCount += len;
+
+                long totalTime = System.currentTimeMillis() - speedTime;
+                if (totalTime < 400) continue;
+
+                long speed = speedCount * 1000 / totalTime;
+
+                if (contentLength != 0) {
+                    int progress = (int) (oldCount * 100 / contentLength);
+                    if (progress != oldProgress || speed != oldSpeed) {
+                        oldProgress = progress;
+                        oldSpeed = speed;
+                        speedCount = 0;
+                        speedTime = System.currentTimeMillis();
+
+                        mProgressBar.onProgress(oldProgress, oldCount, oldSpeed);
+                    }
+                } else if (oldSpeed != speed) {
+                    speedCount = 0;
+                    oldSpeed = speed;
+                    speedTime = System.currentTimeMillis();
+
+                    mProgressBar.onProgress(0, oldCount, oldSpeed);
+                } else {
+                    mProgressBar.onProgress(0, oldCount, oldSpeed);
+                }
+            }
+            mProgressBar.onProgress(100, oldCount, oldSpeed);
+
+            //noinspection ResultOfMethodCallIgnored
+            tempFile.renameTo(file);
+            return file.getAbsolutePath();
+        } finally {
+            IOUtils.closeQuietly(response);
+        }
+    }
+
+    /**
+     * Perform a network request.
+     *
+     * @param download target request.
+     * @return {@link Response}.
+     * @throws IOException when connecting to the network, write data, read the data {@link IOException} occurred.
+     */
+    protected abstract Response requestNetwork(T download) throws IOException;
+
+    /**
+     * Cancel request.
+     */
+    public abstract void cancel();
+
+    private String getRealFileName(Headers headers) throws IOException {
+        String fileName = null;
+        String contentDisposition = headers.getContentDisposition();
+        if (!TextUtils.isEmpty(contentDisposition)) {
+            fileName = Headers.parseSubValue(contentDisposition, "filename", null);
+            if (!TextUtils.isEmpty(fileName)) {
+                fileName = UrlUtils.urlDecode(fileName, "utf-8");
+                if (fileName.startsWith("\"") && fileName.endsWith("\"")) {
+                    fileName = fileName.substring(1, fileName.length() - 1);
+                }
+            }
+        }
+
+        // From url.
+        if (TextUtils.isEmpty(fileName)) {
+            Url url = mDownload.url();
+            String path = url.getPath();
+            if (TextUtils.isEmpty(path)) {
+                fileName = Integer.toString(url.toString().hashCode());
+            } else {
+                String[] slash = path.split("/");
+                fileName = slash[slash.length - 1];
+            }
+        }
+        return fileName;
+    }
+
+    private static class AsyncProgressBar implements Download.ProgressBar {
+
+        private final Download.ProgressBar mProgressBar;
+        private final Executor mExecutor;
+
+        AsyncProgressBar(Download.ProgressBar bar) {
+            this.mProgressBar = bar;
+            this.mExecutor = Kalle.getConfig().getMainExecutor();
+        }
+
+        @Override
+        public void onProgress(final int progress, final long byteCount, final long speed) {
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mProgressBar.onProgress(progress, byteCount, speed);
+                }
+            });
+        }
+    }
+}

+ 104 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/BodyDownload.java

@@ -0,0 +1,104 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+import com.yanzhenjie.kalle.BodyRequest;
+import com.yanzhenjie.kalle.Canceller;
+import com.yanzhenjie.kalle.RequestMethod;
+import com.yanzhenjie.kalle.Url;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public class BodyDownload extends BodyRequest implements Download {
+
+    private final String mDirectory;
+    private final String mFileName;
+    private final ProgressBar mProgressBar;
+    private final Policy mPolicy;
+    private BodyDownload(Api api) {
+        super(api);
+        this.mDirectory = api.mDirectory;
+        this.mFileName = api.mFileName;
+        this.mProgressBar = api.mProgressBar == null ? ProgressBar.DEFAULT : api.mProgressBar;
+        this.mPolicy = api.mPolicy == null ? Policy.DEFAULT : api.mPolicy;
+    }
+
+    public static BodyDownload.Api newApi(Url url, RequestMethod method) {
+        return new BodyDownload.Api(url, method);
+    }
+
+    @Override
+    public String directory() {
+        return mDirectory;
+    }
+
+    @Override
+    public String fileName() {
+        return mFileName;
+    }
+
+    @Override
+    public ProgressBar progressBar() {
+        return mProgressBar;
+    }
+
+    @Override
+    public Policy policy() {
+        return mPolicy;
+    }
+
+    public static class Api extends BodyRequest.Api<BodyDownload.Api> {
+
+        private String mDirectory;
+        private String mFileName;
+
+        private ProgressBar mProgressBar;
+        private Policy mPolicy;
+
+        private Api(Url url, RequestMethod method) {
+            super(url, method);
+        }
+
+        public Api directory(String directory) {
+            this.mDirectory = directory;
+            return this;
+        }
+
+        public Api fileName(String fileName) {
+            this.mFileName = fileName;
+            return this;
+        }
+
+        public Api onProgress(ProgressBar bar) {
+            this.mProgressBar = bar;
+            return this;
+        }
+
+        public Api policy(Policy policy) {
+            this.mPolicy = policy;
+            return this;
+        }
+
+        public String perform() throws Exception {
+            return new BodyWorker(new BodyDownload(this)).call();
+        }
+
+        public Canceller perform(Callback callback) {
+            return DownloadManager.getInstance().perform(new BodyDownload(this), callback);
+        }
+    }
+}

+ 46 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/BodyWorker.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.connect.http.Call;
+
+import java.io.IOException;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public class BodyWorker extends BasicWorker<BodyDownload> {
+
+    private Call mCall;
+
+    BodyWorker(BodyDownload download) {
+        super(download);
+    }
+
+    @Override
+    protected Response requestNetwork(BodyDownload download) throws IOException {
+        mCall = new Call(download);
+        return mCall.execute();
+    }
+
+    @Override
+    public void cancel() {
+        if (mCall != null && !mCall.isCanceled()) {
+            mCall.asyncCancel();
+        }
+    }
+}

+ 46 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/Callback.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public interface Callback {
+    /**
+     * Time dimensions: The request started.
+     */
+    void onStart();
+
+    /**
+     * Result dimensions: File download completed.
+     */
+    void onFinish(String path);
+
+    /**
+     * Result dimensions: An abnormality has occurred.
+     */
+    void onException(Exception e);
+
+    /**
+     * Result dimensions: The request was cancelled.
+     */
+    void onCancel();
+
+    /**
+     * Time dimensions: The request ended.
+     */
+    void onEnd();
+}

+ 114 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/Download.java

@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.Url;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public interface Download {
+
+    /**
+     * Get the file download address.
+     */
+    Url url();
+
+    /**
+     * Get headers.
+     */
+    Headers headers();
+
+    /**
+     * Get the directory where the file is to be saved.
+     */
+    String directory();
+
+    /**
+     * Get the file name.
+     */
+    String fileName();
+
+    /**
+     * Get onProgress bar.
+     */
+    ProgressBar progressBar();
+
+    /**
+     * Get download policy.
+     */
+    Policy policy();
+
+    interface Policy {
+
+        Policy DEFAULT = new Policy() {
+            @Override
+            public boolean isRange() {
+                return true;
+            }
+
+            @Override
+            public boolean allowDownload(int code, Headers headers) {
+                return true;
+            }
+
+            @Override
+            public boolean oldAvailable(String path, int code, Headers headers) {
+                return false;
+            }
+        };
+
+        /**
+         * Does it support breakpoints?
+         */
+        boolean isRange();
+
+        /**
+         * Can I download it?
+         *
+         * @param code    http response code.
+         * @param headers http response headers.
+         * @return return true to continue the download, return false will call back the download failed.
+         */
+        boolean allowDownload(int code, Headers headers);
+
+        /**
+         * Discover old files. The file will be returned if it is available,
+         * the file will be deleted if it is not available.
+         *
+         * @param path    old file path.
+         * @param code    http response code.
+         * @param headers http response headers.
+         * @return return true if the old file is available, other wise is false.
+         */
+        boolean oldAvailable(String path, int code, Headers headers);
+    }
+
+    interface ProgressBar {
+
+        ProgressBar DEFAULT = new ProgressBar() {
+            @Override
+            public void onProgress(int progress, long byteCount, long speed) {
+            }
+        };
+
+        /**
+         * Download onProgress changes.
+         */
+        void onProgress(int progress, long byteCount, long speed);
+    }
+}

+ 182 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/DownloadManager.java

@@ -0,0 +1,182 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+import com.yanzhenjie.kalle.CancelerManager;
+import com.yanzhenjie.kalle.Canceller;
+import com.yanzhenjie.kalle.Kalle;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public class DownloadManager {
+
+    private static DownloadManager sInstance;
+    private final Executor mExecutor;
+    private final CancelerManager mCancelManager;
+    private DownloadManager() {
+        this.mExecutor = Kalle.getConfig().getWorkExecutor();
+        this.mCancelManager = new CancelerManager();
+    }
+
+    public static DownloadManager getInstance() {
+        if (sInstance == null) {
+            synchronized (DownloadManager.class) {
+                if (sInstance == null) {
+                    sInstance = new DownloadManager();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    /**
+     * Submit a request to the queue.
+     *
+     * @param download download request.
+     * @param callback accept the result callback.
+     * @return this request corresponds to the task cancel handle.
+     */
+    public Canceller perform(final UrlDownload download, Callback callback) {
+        final Work<UrlDownload> work = new Work<>(new UrlWorker(download), new AsyncCallback(callback) {
+            @Override
+            public void onEnd() {
+                super.onEnd();
+                mCancelManager.removeCancel(download);
+            }
+        });
+        mCancelManager.addCancel(download, work);
+        mExecutor.execute(work);
+        return work;
+    }
+
+    /**
+     * Execute a request.
+     *
+     * @param download download request.
+     * @return download the completed file path.
+     */
+    public String perform(UrlDownload download) throws Exception {
+        return new UrlWorker(download).call();
+    }
+
+    /**
+     * Submit a request to the queue.
+     *
+     * @param download download request.
+     * @param callback accept the result callback.
+     * @return this request corresponds to the task cancel handle.
+     */
+    public Canceller perform(final BodyDownload download, Callback callback) {
+        final Work<BodyDownload> work = new Work<>(new BodyWorker(download), new AsyncCallback(callback) {
+            @Override
+            public void onEnd() {
+                super.onEnd();
+                mCancelManager.removeCancel(download);
+            }
+        });
+        mCancelManager.addCancel(download, work);
+        mExecutor.execute(work);
+        return work;
+    }
+
+    /**
+     * Execute a request.
+     *
+     * @param download download request.
+     * @return download the completed file path.
+     */
+    public String perform(BodyDownload download) throws Exception {
+        return new BodyWorker(download).call();
+    }
+
+    /**
+     * Cancel multiple requests based on tag.
+     *
+     * @param tag Specified tag.
+     */
+    public void cancel(Object tag) {
+        mCancelManager.cancel(tag);
+    }
+
+    private static class AsyncCallback implements Callback {
+
+        private final Callback mCallback;
+        private final Executor mExecutor;
+
+        AsyncCallback(Callback callback) {
+            this.mCallback = callback;
+            this.mExecutor = Kalle.getConfig().getMainExecutor();
+        }
+
+        @Override
+        public void onStart() {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onStart();
+                }
+            });
+        }
+
+        @Override
+        public void onFinish(final String path) {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onFinish(path);
+                }
+            });
+        }
+
+        @Override
+        public void onException(final Exception e) {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onException(e);
+                }
+            });
+        }
+
+        @Override
+        public void onCancel() {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onCancel();
+                }
+            });
+        }
+
+        @Override
+        public void onEnd() {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onEnd();
+                }
+            });
+        }
+    }
+}

+ 41 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/SimpleCallback.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public class SimpleCallback implements Callback {
+    @Override
+    public void onStart() {
+    }
+
+    @Override
+    public void onFinish(String path) {
+    }
+
+    @Override
+    public void onException(Exception e) {
+    }
+
+    @Override
+    public void onCancel() {
+    }
+
+    @Override
+    public void onEnd() {
+    }
+}

+ 104 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/UrlDownload.java

@@ -0,0 +1,104 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+import com.yanzhenjie.kalle.Canceller;
+import com.yanzhenjie.kalle.RequestMethod;
+import com.yanzhenjie.kalle.Url;
+import com.yanzhenjie.kalle.UrlRequest;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public class UrlDownload extends UrlRequest implements Download {
+
+    private final String mDirectory;
+    private final String mFileName;
+    private final ProgressBar mProgressBar;
+    private final Policy mPolicy;
+    private UrlDownload(Api api) {
+        super(api);
+        this.mDirectory = api.mDirectory;
+        this.mFileName = api.mFileName;
+        this.mProgressBar = api.mProgressBar == null ? ProgressBar.DEFAULT : api.mProgressBar;
+        this.mPolicy = api.mPolicy == null ? Policy.DEFAULT : api.mPolicy;
+    }
+
+    public static UrlDownload.Api newApi(Url url, RequestMethod method) {
+        return new UrlDownload.Api(url, method);
+    }
+
+    @Override
+    public String directory() {
+        return mDirectory;
+    }
+
+    @Override
+    public String fileName() {
+        return mFileName;
+    }
+
+    @Override
+    public ProgressBar progressBar() {
+        return mProgressBar;
+    }
+
+    @Override
+    public Policy policy() {
+        return mPolicy;
+    }
+
+    public static class Api extends UrlRequest.Api<UrlDownload.Api> {
+
+        private String mDirectory;
+        private String mFileName;
+
+        private ProgressBar mProgressBar;
+        private Policy mPolicy;
+
+        private Api(Url url, RequestMethod method) {
+            super(url, method);
+        }
+
+        public Api directory(String directory) {
+            this.mDirectory = directory;
+            return this;
+        }
+
+        public Api fileName(String fileName) {
+            this.mFileName = fileName;
+            return this;
+        }
+
+        public Api onProgress(ProgressBar bar) {
+            this.mProgressBar = bar;
+            return this;
+        }
+
+        public Api policy(Policy policy) {
+            this.mPolicy = policy;
+            return this;
+        }
+
+        public String perform() throws Exception {
+            return new UrlWorker(new UrlDownload(this)).call();
+        }
+
+        public Canceller perform(Callback callback) {
+            return DownloadManager.getInstance().perform(new UrlDownload(this), callback);
+        }
+    }
+}

+ 46 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/UrlWorker.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.connect.http.Call;
+
+import java.io.IOException;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public class UrlWorker extends BasicWorker<UrlDownload> {
+
+    private Call mCall;
+
+    UrlWorker(UrlDownload download) {
+        super(download);
+    }
+
+    @Override
+    protected Response requestNetwork(UrlDownload download) throws IOException {
+        mCall = new Call(download);
+        return mCall.execute();
+    }
+
+    @Override
+    public void cancel() {
+        if (mCall != null && !mCall.isCanceled()) {
+            mCall.asyncCancel();
+        }
+    }
+}

+ 74 - 0
kalle/src/main/java/com/yanzhenjie/kalle/download/Work.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.download;
+
+import com.yanzhenjie.kalle.Canceller;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public class Work<T extends Download> extends FutureTask<String> implements Canceller {
+
+    private final Callback mCallback;
+    private BasicWorker<T> mWorker;
+
+    Work(BasicWorker<T> work, Callback callback) {
+        super(work);
+        this.mWorker = work;
+        this.mCallback = callback;
+    }
+
+    @Override
+    public void run() {
+        mCallback.onStart();
+        super.run();
+    }
+
+    @Override
+    protected void done() {
+        try {
+            mCallback.onFinish(get());
+        } catch (CancellationException e) {
+            mCallback.onCancel();
+        } catch (ExecutionException e) {
+            Throwable cause = e.getCause();
+            if (isCancelled()) {
+                mCallback.onCancel();
+            } else if (cause != null && cause instanceof Exception) {
+                mCallback.onException((Exception) cause);
+            } else {
+                mCallback.onException(new Exception(cause));
+            }
+        } catch (Exception e) {
+            if (isCancelled()) {
+                mCallback.onCancel();
+            } else {
+                mCallback.onException(e);
+            }
+        }
+        mCallback.onEnd();
+    }
+
+    @Override
+    public void cancel() {
+        cancel(true);
+        mWorker.cancel();
+    }
+}

+ 36 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/ConnectException.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+import java.io.IOException;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public class ConnectException extends IOException {
+
+    public ConnectException(String message) {
+        super(message);
+    }
+
+    public ConnectException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ConnectException(Throwable cause) {
+        super(cause);
+    }
+}

+ 29 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/ConnectTimeoutError.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/22.
+ */
+public class ConnectTimeoutError extends ConnectException {
+    public ConnectTimeoutError(String message) {
+        super(message);
+    }
+
+    public ConnectTimeoutError(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 47 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/DownloadError.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+import com.yanzhenjie.kalle.Headers;
+
+/**
+ * Created by Zhenjie Yan on 2018/3/18.
+ */
+public class DownloadError extends ReadException {
+
+    private int mCode;
+    private Headers mHeaders;
+
+    public DownloadError(int code, Headers headers, String message) {
+        super(message);
+        this.mCode = code;
+        this.mHeaders = headers;
+    }
+
+    public DownloadError(int code, Headers headers, Throwable cause) {
+        super(cause);
+        this.mCode = code;
+        this.mHeaders = headers;
+    }
+
+    public int getCode() {
+        return mCode;
+    }
+
+    public Headers getHeaders() {
+        return mHeaders;
+    }
+}

+ 29 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/HostError.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/22.
+ */
+public class HostError extends ConnectException {
+    public HostError(String message) {
+        super(message);
+    }
+
+    public HostError(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 29 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/NetworkError.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/22.
+ */
+public class NetworkError extends ConnectException {
+    public NetworkError(String message) {
+        super(message);
+    }
+
+    public NetworkError(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 29 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/NoCacheError.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/26.
+ */
+public class NoCacheError extends ReadException {
+    public NoCacheError(String message) {
+        super(message);
+    }
+
+    public NoCacheError(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 29 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/ParseError.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/27.
+ */
+public class ParseError extends ReadException {
+    public ParseError(String message) {
+        super(message);
+    }
+
+    public ParseError(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 36 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/ReadException.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+import java.io.IOException;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public class ReadException extends IOException {
+
+    public ReadException(String message) {
+        super(message);
+    }
+
+    public ReadException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ReadException(Throwable cause) {
+        super(cause);
+    }
+}

+ 29 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/ReadTimeoutError.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/24.
+ */
+public class ReadTimeoutError extends ReadException {
+    public ReadTimeoutError(String message) {
+        super(message);
+    }
+
+    public ReadTimeoutError(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 29 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/URLError.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/22.
+ */
+public class URLError extends ConnectException {
+    public URLError(String message) {
+        super(message);
+    }
+
+    public URLError(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 35 - 0
kalle/src/main/java/com/yanzhenjie/kalle/exception/WriteException.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.exception;
+
+import java.io.IOException;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/24.
+ */
+public class WriteException extends IOException {
+    public WriteException(String message) {
+        super(message);
+    }
+
+    public WriteException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public WriteException(Throwable cause) {
+        super(cause);
+    }
+}

+ 67 - 0
kalle/src/main/java/com/yanzhenjie/kalle/secure/AESSecret.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.secure;
+
+import java.security.GeneralSecurityException;
+import java.security.Key;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public class AESSecret implements Secret {
+
+    private Cipher encrypt;
+    private Cipher decrypt;
+
+    public AESSecret(String key) throws GeneralSecurityException {
+        Key cryptKey = getKey(key.getBytes());
+        encrypt = Cipher.getInstance("AES");
+        encrypt.init(Cipher.ENCRYPT_MODE, cryptKey);
+        decrypt = Cipher.getInstance("AES");
+        decrypt.init(Cipher.DECRYPT_MODE, cryptKey);
+    }
+
+    @Override
+    public String encrypt(String data) throws GeneralSecurityException {
+        return Encryption.byteArrayToHex(encrypt(data.getBytes()));
+    }
+
+    @Override
+    public byte[] encrypt(byte[] data) throws GeneralSecurityException {
+        return encrypt.doFinal(data);
+    }
+
+    @Override
+    public String decrypt(String data) throws GeneralSecurityException {
+        return new String(decrypt(Encryption.hexToByteArray(data)));
+    }
+
+    @Override
+    public byte[] decrypt(byte[] data) throws GeneralSecurityException {
+        return decrypt.doFinal(data);
+    }
+
+    private Key getKey(byte[] keyData) {
+        byte[] arrB = new byte[8];
+        for (int i = 0; i < keyData.length && i < arrB.length; i++) {
+            arrB[i] = keyData[i];
+        }
+        return new SecretKeySpec(arrB, "AES");
+    }
+}

+ 96 - 0
kalle/src/main/java/com/yanzhenjie/kalle/secure/Encryption.java

@@ -0,0 +1,96 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.secure;
+
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Created in 2016/4/10 11:27.
+ */
+public class Encryption {
+
+    /**
+     * Create a secret of encryption and decryption.
+     */
+    public static Secret createSecret(String key) {
+        try {
+            return new AESSecret(key);
+        } catch (GeneralSecurityException e) {
+            return new SafeSecret();
+        }
+    }
+
+    /**
+     * Byte array turn to hex string.
+     */
+    public static String byteArrayToHex(byte[] byteArray) {
+        int len = byteArray.length;
+        StringBuilder sb = new StringBuilder(len * 2);
+        for (int i = 0; i < len; i++) {
+            int intTmp = byteArray[i];
+            while (intTmp < 0) {
+                intTmp = intTmp + 256;
+            }
+            if (intTmp < 16) {
+                sb.append("0");
+            }
+            sb.append(Integer.toString(intTmp, 16));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Hex string turn to byte array.
+     */
+    public static byte[] hexToByteArray(String hexString) {
+        byte[] byteArrayIn = hexString.getBytes();
+        int iLen = byteArrayIn.length;
+        byte[] byteArrayOut = new byte[iLen / 2];
+        for (int i = 0; i < iLen; i = i + 2) {
+            String strTmp = new String(byteArrayIn, i, 2);
+            byteArrayOut[i / 2] = (byte) Integer.parseInt(strTmp, 16);
+        }
+        return byteArrayOut;
+    }
+
+    /**
+     * Get the md5 value of string.
+     */
+    public static String getMD5ForString(String content) {
+        try {
+            StringBuilder md5Builder = new StringBuilder();
+            MessageDigest digest = MessageDigest.getInstance("MD5");
+            byte[] tempBytes = digest.digest(content.getBytes());
+            int digital;
+            for (byte tempByte : tempBytes) {
+                digital = tempByte;
+                if (digital < 0) {
+                    digital += 256;
+                }
+                if (digital < 16) {
+                    md5Builder.append("0");
+                }
+                md5Builder.append(Integer.toHexString(digital));
+            }
+            return md5Builder.toString();
+        } catch (NoSuchAlgorithmException ignored) {
+        }
+        return content;
+    }
+
+}

+ 43 - 0
kalle/src/main/java/com/yanzhenjie/kalle/secure/SafeSecret.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.secure;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public class SafeSecret implements Secret {
+    @Override
+    public String encrypt(String data) throws GeneralSecurityException {
+        return data;
+    }
+
+    @Override
+    public byte[] encrypt(byte[] data) throws GeneralSecurityException {
+        return data;
+    }
+
+    @Override
+    public String decrypt(String data) throws GeneralSecurityException {
+        return data;
+    }
+
+    @Override
+    public byte[] decrypt(byte[] data) throws GeneralSecurityException {
+        return data;
+    }
+}

+ 32 - 0
kalle/src/main/java/com/yanzhenjie/kalle/secure/Secret.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.secure;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/11.
+ */
+public interface Secret {
+
+    String encrypt(String data) throws GeneralSecurityException;
+
+    byte[] encrypt(byte[] data) throws GeneralSecurityException;
+
+    String decrypt(String data) throws GeneralSecurityException;
+
+    byte[] decrypt(byte[] data) throws GeneralSecurityException;
+}

+ 296 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/BasicWorker.java

@@ -0,0 +1,296 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.Kalle;
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.exception.NoCacheError;
+import com.yanzhenjie.kalle.exception.ParseError;
+import com.yanzhenjie.kalle.simple.cache.Cache;
+import com.yanzhenjie.kalle.simple.cache.CacheMode;
+import com.yanzhenjie.kalle.simple.cache.CacheStore;
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.concurrent.Callable;
+
+import static com.yanzhenjie.kalle.Headers.KEY_IF_MODIFIED_SINCE;
+import static com.yanzhenjie.kalle.Headers.KEY_IF_NONE_MATCH;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/18.
+ */
+abstract class BasicWorker<T extends SimpleRequest, Succeed, Failed>
+        implements Callable<SimpleResponse<Succeed, Failed>> {
+
+    private static final long MAX_EXPIRES = System.currentTimeMillis() + 100L * 365L * 24L * 60L * 60L * 1000L;
+
+    private final T mRequest;
+    private final CacheStore mCacheStore;
+    private final Converter mConverter;
+    private final Type mSucceed;
+    private final Type mFailed;
+
+    BasicWorker(T request, Type succeed, Type failed) {
+        this.mRequest = request;
+        this.mSucceed = succeed;
+        this.mFailed = failed;
+        this.mCacheStore = Kalle.getConfig().getCacheStore();
+        this.mConverter = request.converter() == null ? Kalle.getConfig().getConverter() : request.converter();
+    }
+
+    @Override
+    public final SimpleResponse<Succeed, Failed> call() throws Exception {
+        Response response = tryReadCacheBefore();
+        if (response != null) return buildSimpleResponse(response, true);
+
+        tryAttachCache();
+
+        try {
+            response = requestNetwork(mRequest);
+
+            int code = response.code();
+            if (code == 304) {
+                Response cacheResponse = tryReadCacheAfter(-1);
+                if (cacheResponse != null) {
+                    return buildSimpleResponse(cacheResponse, true);
+                } else {
+                    return buildSimpleResponse(response, false);
+                }
+            }
+            Headers headers = response.headers();
+            byte[] body = {};
+            if (code != 204) {
+                body = response.body().byteArray();
+            }
+            IOUtils.closeQuietly(response);
+
+            tryDetachCache(code, headers, body);
+
+            response = buildResponse(code, headers, body);
+            return buildSimpleResponse(response, false);
+        } catch (IOException e) {
+            Response cacheResponse = tryReadCacheAfter(-1);
+            if (cacheResponse != null) {
+                return buildSimpleResponse(cacheResponse, true);
+            }
+            throw e;
+        } finally {
+            IOUtils.closeQuietly(response);
+        }
+    }
+
+    /**
+     * Perform a network request.
+     *
+     * @param request target request.
+     * @return {@link Response}.
+     * @throws IOException when connecting to the network, write data, read the data {@link IOException} occurred.
+     */
+    protected abstract Response requestNetwork(T request) throws IOException;
+
+    /**
+     * Cancel request.
+     */
+    public abstract void cancel();
+
+    private Response tryReadCacheBefore() throws NoCacheError {
+        CacheMode cacheMode = mRequest.cacheMode();
+        switch (cacheMode) {
+            case HTTP: {
+                Cache cache = mCacheStore.get(mRequest.cacheKey());
+                if (cache == null) return null;
+                if (cache.getExpires() > System.currentTimeMillis()) {
+                    return buildResponse(cache.getCode(), cache.getHeaders(), cache.getBody());
+                }
+                break;
+            }
+            case HTTP_YES_THEN_WRITE_CACHE:
+            case NETWORK:
+            case NETWORK_YES_THEN_HTTP:
+            case NETWORK_YES_THEN_WRITE_CACHE:
+            case NETWORK_NO_THEN_READ_CACHE: {
+                // Nothing.
+                return null;
+            }
+            case READ_CACHE: {
+                Cache cache = mCacheStore.get(mRequest.cacheKey());
+                if (cache != null) {
+                    return buildResponse(cache.getCode(), cache.getHeaders(), cache.getBody());
+                }
+                throw new NoCacheError("No cache found.");
+            }
+            case READ_CACHE_NO_THEN_NETWORK:
+            case READ_CACHE_NO_THEN_HTTP: {
+                Cache cache = mCacheStore.get(mRequest.cacheKey());
+                if (cache != null) {
+                    return buildResponse(cache.getCode(), cache.getHeaders(), cache.getBody());
+                }
+                break;
+            }
+        }
+        return null;
+    }
+
+    private void tryAttachCache() {
+        CacheMode cacheMode = mRequest.cacheMode();
+        switch (cacheMode) {
+            case HTTP:
+            case HTTP_YES_THEN_WRITE_CACHE: {
+                Cache cacheEntity = mCacheStore.get(mRequest.cacheKey());
+                if (cacheEntity != null) attachCache(cacheEntity.getHeaders());
+                break;
+            }
+            case NETWORK:
+            case NETWORK_YES_THEN_HTTP:
+            case NETWORK_YES_THEN_WRITE_CACHE:
+            case NETWORK_NO_THEN_READ_CACHE:
+            case READ_CACHE:
+            case READ_CACHE_NO_THEN_NETWORK:
+            case READ_CACHE_NO_THEN_HTTP: {
+                // Nothing.
+                break;
+            }
+        }
+    }
+
+    private void tryDetachCache(int code, Headers headers, byte[] body) {
+        CacheMode cacheMode = mRequest.cacheMode();
+        switch (cacheMode) {
+            case HTTP: {
+                long expires = Headers.analysisCacheExpires(headers);
+                if (expires > 0 || headers.getLastModified() > 0) {
+                    detachCache(code, headers, body, expires);
+                }
+                break;
+            }
+            case HTTP_YES_THEN_WRITE_CACHE: {
+                detachCache(code, headers, body, MAX_EXPIRES);
+                break;
+            }
+            case NETWORK: {
+                break;
+            }
+            case NETWORK_YES_THEN_HTTP: {
+                long expires = Headers.analysisCacheExpires(headers);
+                if (expires > 0 || headers.getLastModified() > 0) {
+                    detachCache(code, headers, body, expires);
+                }
+                break;
+            }
+            case NETWORK_YES_THEN_WRITE_CACHE: {
+                detachCache(code, headers, body, MAX_EXPIRES);
+                break;
+            }
+            case NETWORK_NO_THEN_READ_CACHE:
+            case READ_CACHE:
+            case READ_CACHE_NO_THEN_NETWORK: {
+                break;
+            }
+            case READ_CACHE_NO_THEN_HTTP: {
+                long expires = Headers.analysisCacheExpires(headers);
+                if (expires > 0 || headers.getLastModified() > 0) {
+                    detachCache(code, headers, body, expires);
+                }
+                break;
+            }
+        }
+    }
+
+    private Response tryReadCacheAfter(int code) {
+        CacheMode cacheMode = mRequest.cacheMode();
+        switch (cacheMode) {
+            case HTTP:
+            case HTTP_YES_THEN_WRITE_CACHE: {
+                if (code == 304) {
+                    Cache cache = mCacheStore.get(mRequest.cacheKey());
+                    if (cache != null) {
+                        return buildResponse(cache.getCode(), cache.getHeaders(), cache.getBody());
+                    }
+                }
+                break;
+            }
+            case NETWORK:
+            case NETWORK_YES_THEN_HTTP:
+            case NETWORK_YES_THEN_WRITE_CACHE: {
+                break;
+            }
+            case NETWORK_NO_THEN_READ_CACHE: {
+                Cache cache = mCacheStore.get(mRequest.cacheKey());
+                if (cache != null) {
+                    return buildResponse(cache.getCode(), cache.getHeaders(), cache.getBody());
+                }
+                break;
+            }
+            case READ_CACHE:
+            case READ_CACHE_NO_THEN_NETWORK: {
+                break;
+            }
+            case READ_CACHE_NO_THEN_HTTP: {
+                if (code == 304) {
+                    Cache cache = mCacheStore.get(mRequest.cacheKey());
+                    if (cache != null) {
+                        return buildResponse(cache.getCode(), cache.getHeaders(), cache.getBody());
+                    }
+                }
+                break;
+            }
+        }
+        return null;
+    }
+
+    private void attachCache(Headers cacheHeaders) {
+        Headers headers = mRequest.headers();
+        String eTag = cacheHeaders.getETag();
+        if (eTag != null) headers.set(KEY_IF_NONE_MATCH, eTag);
+
+        long lastModified = cacheHeaders.getLastModified();
+        if (lastModified > 0)
+            headers.set(KEY_IF_MODIFIED_SINCE, Headers.formatMillisToGMT(lastModified));
+    }
+
+    private void detachCache(int code, Headers headers, byte[] body, long expires) {
+        String cacheKey = mRequest.cacheKey();
+
+        Cache entity = new Cache();
+        entity.setKey(cacheKey);
+        entity.setCode(code);
+        entity.setHeaders(headers);
+        entity.setBody(body);
+        entity.setExpires(expires);
+        mCacheStore.replace(cacheKey, entity);
+    }
+
+    private Response buildResponse(int code, Headers headers, byte[] body) {
+        return Response.newBuilder()
+                .code(code)
+                .headers(headers)
+                .body(new ByteArrayBody(headers.getContentType(), body))
+                .build();
+    }
+
+    private SimpleResponse<Succeed, Failed> buildSimpleResponse(Response response, boolean cache) throws IOException {
+        try {
+            return mConverter.convert(mSucceed, mFailed, response, cache);
+        } catch (IOException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ParseError("An exception occurred while parsing the data.", e);
+        }
+    }
+}

+ 47 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/BodyWorker.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.connect.http.Call;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+final class BodyWorker<S, F> extends BasicWorker<SimpleBodyRequest, S, F> {
+
+    private Call mCall;
+
+    BodyWorker(SimpleBodyRequest request, Type succeed, Type failed) {
+        super(request, succeed, failed);
+    }
+
+    @Override
+    protected Response requestNetwork(SimpleBodyRequest request) throws IOException {
+        mCall = new Call(request);
+        return mCall.execute();
+    }
+
+    @Override
+    public void cancel() {
+        if (mCall != null && !mCall.isCanceled()) {
+            mCall.asyncCancel();
+        }
+    }
+}

+ 61 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/ByteArrayBody.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.ResponseBody;
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/22.
+ */
+public class ByteArrayBody implements ResponseBody {
+
+    private String mContentType;
+    private byte[] mData;
+
+    public ByteArrayBody(String contentType, byte[] data) {
+        this.mContentType = contentType;
+        this.mData = data;
+    }
+
+    @Override
+    public String string() throws IOException {
+        String charset = Headers.parseSubValue(mContentType, "charset", null);
+        return TextUtils.isEmpty(charset) ? IOUtils.toString(mData) : IOUtils.toString(mData, charset);
+    }
+
+    @Override
+    public byte[] byteArray() throws IOException {
+        return mData;
+    }
+
+    @Override
+    public InputStream stream() throws IOException {
+        return new ByteArrayInputStream(mData);
+    }
+
+    @Override
+    public void close() throws IOException {
+        mData = null;
+    }
+}

+ 69 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/Callback.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/17.
+ */
+public abstract class Callback<Succeed, Failed> {
+
+    public Callback() {
+    }
+
+    /**
+     * Get the data type when the business was successful.
+     */
+    public Type getSucceed() {
+        Type superClass = getClass().getGenericSuperclass();
+        return ((ParameterizedType) superClass).getActualTypeArguments()[0];
+    }
+
+    /**
+     * Get the data type when the business failed.
+     */
+    public Type getFailed() {
+        Type superClass = getClass().getGenericSuperclass();
+        return ((ParameterizedType) superClass).getActualTypeArguments()[1];
+    }
+
+    /**
+     * Time dimensions: The request started.
+     */
+    public abstract void onStart();
+
+    /**
+     * Result dimensions: The request has responded.
+     */
+    public abstract void onResponse(SimpleResponse<Succeed, Failed> response);
+
+    /**
+     * Result dimensions: An abnormality has occurred.
+     */
+    public abstract void onException(Exception e);
+
+    /**
+     * Result dimensions: The request was cancelled.
+     */
+    public abstract void onCancel();
+
+    /**
+     * Time dimensions: The request ended.
+     */
+    public abstract void onEnd();
+}

+ 59 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/Converter.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import com.yanzhenjie.kalle.Response;
+
+import java.lang.reflect.Type;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/12.
+ */
+public interface Converter {
+
+    /**
+     * Default converter.
+     */
+    Converter DEFAULT = new Converter() {
+        @Override
+        public <S, F> SimpleResponse<S, F> convert(Type succeed, Type failed, Response response, boolean fromCache) throws Exception {
+            S succeedData = null;
+
+            if (succeed == String.class) succeedData = (S) response.body().string();
+
+            return SimpleResponse.<S, F>newBuilder()
+                    .code(response.code())
+                    .headers(response.headers())
+                    .fromCache(fromCache)
+                    .succeed(succeedData)
+                    .build();
+        }
+    };
+
+    /**
+     * Convert data to the result of the target type.
+     *
+     * @param succeed   the data type when the business succeed.
+     * @param failed    the data type when the business failed.
+     * @param response  response of request.
+     * @param fromCache the response is from the cache.
+     * @param <S>       the data type.
+     * @param <F>       the data type.
+     * @return {@link SimpleResponse}
+     * @throws Exception to prevent accidents.
+     */
+    <S, F> SimpleResponse<S, F> convert(Type succeed, Type failed, Response response, boolean fromCache) throws Exception;
+}

+ 205 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/RequestManager.java

@@ -0,0 +1,205 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import com.yanzhenjie.kalle.CancelerManager;
+import com.yanzhenjie.kalle.Canceller;
+import com.yanzhenjie.kalle.Kalle;
+
+import java.lang.reflect.Type;
+import java.util.concurrent.Executor;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public class RequestManager {
+
+    private static RequestManager sInstance;
+    private final Executor mExecutor;
+    private final CancelerManager mCancelManager;
+    private RequestManager() {
+        this.mExecutor = Kalle.getConfig().getWorkExecutor();
+        this.mCancelManager = new CancelerManager();
+    }
+
+    public static RequestManager getInstance() {
+        if (sInstance == null) {
+            synchronized (RequestManager.class) {
+                if (sInstance == null) {
+                    sInstance = new RequestManager();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    /**
+     * Submit a request to the queue.
+     *
+     * @param request  request.
+     * @param callback accept the result callback.
+     * @param <S>      target object parameter.
+     * @param <F>      target object parameter.
+     * @return this request corresponds to the task cancel handle.
+     */
+    public <S, F> Canceller perform(final SimpleUrlRequest request, Callback<S, F> callback) {
+        final Work<SimpleUrlRequest, S, F> work = new Work<>(new UrlWorker<S, F>(request, callback.getSucceed(), callback.getFailed()), new AsyncCallback<S, F>(callback) {
+            @Override
+            public void onEnd() {
+                super.onEnd();
+                mCancelManager.removeCancel(request);
+            }
+        });
+        mCancelManager.addCancel(request, work);
+        mExecutor.execute(work);
+        return work;
+    }
+
+    /**
+     * Execute a request.
+     *
+     * @param request request.
+     * @param succeed the data type when the business succeed.
+     * @param failed  the data type when the business failed.
+     * @param <S>     target object parameter.
+     * @param <F>     target object parameter.
+     * @return the response to this request.
+     */
+    public <S, F> SimpleResponse<S, F> perform(SimpleUrlRequest request, Type succeed, Type failed) throws Exception {
+        return new UrlWorker<S, F>(request, succeed, failed).call();
+    }
+
+    /**
+     * Submit a request to the queue.
+     *
+     * @param request  request.
+     * @param callback accept the result callback.
+     * @param <S>      target object parameter.
+     * @param <F>      target object parameter.
+     * @return this request corresponds to the task cancel handle.
+     */
+    public <S, F> Canceller perform(final SimpleBodyRequest request, Callback<S, F> callback) {
+        Work<SimpleBodyRequest, S, F> work = new Work<>(new BodyWorker<S, F>(request, callback.getSucceed(), callback.getFailed()), new AsyncCallback<S, F>(callback) {
+            @Override
+            public void onEnd() {
+                super.onEnd();
+                mCancelManager.removeCancel(request);
+            }
+        });
+        mCancelManager.addCancel(request, work);
+        mExecutor.execute(work);
+        return work;
+    }
+
+    /**
+     * Execute a request.
+     *
+     * @param request request.
+     * @param succeed the data type when the business succeed.
+     * @param failed  the data type when the business failed.
+     * @param <S>     target object parameter.
+     * @param <F>     target object parameter.
+     * @return the response to this request.
+     */
+    public <S, F> SimpleResponse<S, F> perform(SimpleBodyRequest request, Type succeed, Type failed) throws Exception {
+        return new BodyWorker<S, F>(request, succeed, failed).call();
+    }
+
+    /**
+     * Cancel multiple requests based on tag.
+     *
+     * @param tag specified tag.
+     */
+    public void cancel(Object tag) {
+        mCancelManager.cancel(tag);
+    }
+
+    private static class AsyncCallback<S, F> extends Callback<S, F> {
+
+        private final Callback<S, F> mCallback;
+        private final Executor mExecutor;
+
+        AsyncCallback(Callback<S, F> callback) {
+            this.mCallback = callback;
+            this.mExecutor = Kalle.getConfig().getMainExecutor();
+        }
+
+        @Override
+        public Type getSucceed() {
+            return mCallback.getSucceed();
+        }
+
+        @Override
+        public Type getFailed() {
+            return mCallback.getFailed();
+        }
+
+        @Override
+        public void onStart() {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onStart();
+                }
+            });
+        }
+
+        @Override
+        public void onResponse(final SimpleResponse<S, F> response) {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onResponse(response);
+                }
+            });
+        }
+
+        @Override
+        public void onException(final Exception e) {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onException(e);
+                }
+            });
+        }
+
+        @Override
+        public void onCancel() {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onCancel();
+                }
+            });
+        }
+
+        @Override
+        public void onEnd() {
+            if (mCallback == null) return;
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onEnd();
+                }
+            });
+        }
+    }
+}

+ 100 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleBodyRequest.java

@@ -0,0 +1,100 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.BodyRequest;
+import com.yanzhenjie.kalle.Canceller;
+import com.yanzhenjie.kalle.RequestMethod;
+import com.yanzhenjie.kalle.Url;
+import com.yanzhenjie.kalle.simple.cache.CacheMode;
+
+import java.lang.reflect.Type;
+
+import static com.yanzhenjie.kalle.simple.cache.CacheMode.HTTP;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public class SimpleBodyRequest extends BodyRequest implements SimpleRequest {
+
+    private final CacheMode mCacheMode;
+    private final String mCacheKey;
+    private final Converter mConverter;
+
+    private SimpleBodyRequest(Api api) {
+        super(api);
+        this.mCacheMode = api.mCacheMode == null ? HTTP : api.mCacheMode;
+        this.mCacheKey = TextUtils.isEmpty(api.mCacheKey) ? url().toString() : api.mCacheKey;
+
+        this.mConverter = api.mConverter;
+    }
+
+    public static SimpleBodyRequest.Api newApi(Url url, RequestMethod method) {
+        return new SimpleBodyRequest.Api(url, method);
+    }
+
+    @Override
+    public CacheMode cacheMode() {
+        return mCacheMode;
+    }
+
+    @Override
+    public String cacheKey() {
+        return mCacheKey;
+    }
+
+    @Override
+    public Converter converter() {
+        return mConverter;
+    }
+
+    public static class Api extends BodyRequest.Api<Api> {
+
+        private CacheMode mCacheMode;
+        private String mCacheKey;
+
+        private Converter mConverter;
+
+        private Api(Url url, RequestMethod method) {
+            super(url, method);
+        }
+
+        public Api cacheMode(CacheMode cacheMode) {
+            this.mCacheMode = cacheMode;
+            return this;
+        }
+
+        public Api cacheKey(String cacheKey) {
+            this.mCacheKey = cacheKey;
+            return this;
+        }
+
+        public Api converter(Converter converter) {
+            this.mConverter = converter;
+            return this;
+        }
+
+        public <S, F> SimpleResponse<S, F> perform(Type succeed, Type failed) throws Exception {
+            return RequestManager.getInstance().perform(new SimpleBodyRequest(this), succeed, failed);
+        }
+
+        public <S, F> Canceller perform(Callback<S, F> callback) {
+            return RequestManager.getInstance().perform(new SimpleBodyRequest(this), callback);
+        }
+    }
+}

+ 55 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleCallback.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/26.
+ */
+public abstract class SimpleCallback<V> extends Callback<V, String> {
+
+    public SimpleCallback() {
+    }
+
+    @Override
+    public Type getSucceed() {
+        Type superClass = getClass().getGenericSuperclass();
+        return ((ParameterizedType) superClass).getActualTypeArguments()[0];
+    }
+
+    @Override
+    public Type getFailed() {
+        return String.class;
+    }
+
+    @Override
+    public void onStart() {
+    }
+
+    @Override
+    public void onException(Exception e) {
+    }
+
+    @Override
+    public void onCancel() {
+    }
+
+    @Override
+    public void onEnd() {
+    }
+}

+ 50 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleRequest.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.Url;
+import com.yanzhenjie.kalle.simple.cache.CacheMode;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/20.
+ */
+public interface SimpleRequest {
+    /**
+     * Get the file download address.
+     */
+    Url url();
+
+    /**
+     * Get headers.
+     */
+    Headers headers();
+
+    /**
+     * Get cache mode.
+     */
+    CacheMode cacheMode();
+
+    /**
+     * Get cache key.
+     */
+    String cacheKey();
+
+    /**
+     * Get converter.
+     */
+    Converter converter();
+}

+ 127 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleResponse.java

@@ -0,0 +1,127 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import com.yanzhenjie.kalle.Headers;
+
+/**
+ * Created in Oct 15, 2015 8:55:37 PM.
+ */
+public final class SimpleResponse<Succeed, Failed> {
+
+    private final int mCode;
+    private final Headers mHeaders;
+    private final boolean mFromCache;
+    private final Succeed mSucceed;
+    private final Failed mFailed;
+    private SimpleResponse(Builder<Succeed, Failed> builder) {
+        this.mCode = builder.mCode;
+        this.mHeaders = builder.mHeaders;
+        this.mFromCache = builder.mFromCache;
+
+        this.mSucceed = builder.mSucceed;
+        this.mFailed = builder.mFailed;
+    }
+
+    public static <Succeed, Failed> Builder<Succeed, Failed> newBuilder() {
+        return new Builder<>();
+    }
+
+    /**
+     * Get the headers code of handle.
+     */
+    public int code() {
+        return mCode;
+    }
+
+    /**
+     * Get http headers headers.
+     */
+    public Headers headers() {
+        return mHeaders;
+    }
+
+    /**
+     * Whether the data returned from the cache.
+     *
+     * @return true: the data from cache, false: the data from network.
+     */
+    public boolean fromCache() {
+        return mFromCache;
+    }
+
+    /**
+     * Business successful.
+     */
+    public boolean isSucceed() {
+        return mFailed == null || mSucceed != null;
+    }
+
+    /**
+     * Get business success data.
+     */
+    public Succeed succeed() {
+        return mSucceed;
+    }
+
+    /**
+     * Get business failure data.
+     */
+    public Failed failed() {
+        return mFailed;
+    }
+
+    public static final class Builder<Succeed, Failed> {
+        private int mCode;
+        private Headers mHeaders;
+        private boolean mFromCache;
+
+        private Failed mFailed;
+        private Succeed mSucceed;
+
+        private Builder() {
+        }
+
+        public Builder<Succeed, Failed> code(int code) {
+            this.mCode = code;
+            return this;
+        }
+
+        public Builder<Succeed, Failed> headers(Headers headers) {
+            this.mHeaders = headers;
+            return this;
+        }
+
+        public Builder<Succeed, Failed> fromCache(boolean fromCache) {
+            this.mFromCache = fromCache;
+            return this;
+        }
+
+        public Builder<Succeed, Failed> succeed(Succeed succeed) {
+            this.mSucceed = succeed;
+            return this;
+        }
+
+        public Builder<Succeed, Failed> failed(Failed failed) {
+            this.mFailed = failed;
+            return this;
+        }
+
+        public SimpleResponse<Succeed, Failed> build() {
+            return new SimpleResponse<>(this);
+        }
+    }
+}

+ 100 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/SimpleUrlRequest.java

@@ -0,0 +1,100 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.Canceller;
+import com.yanzhenjie.kalle.RequestMethod;
+import com.yanzhenjie.kalle.Url;
+import com.yanzhenjie.kalle.UrlRequest;
+import com.yanzhenjie.kalle.simple.cache.CacheMode;
+
+import java.lang.reflect.Type;
+
+import static com.yanzhenjie.kalle.simple.cache.CacheMode.HTTP;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+public class SimpleUrlRequest extends UrlRequest implements SimpleRequest {
+
+    private final CacheMode mCacheMode;
+    private final String mCacheKey;
+    private final Converter mConverter;
+
+    private SimpleUrlRequest(Api api) {
+        super(api);
+        this.mCacheMode = api.mCacheMode == null ? HTTP : api.mCacheMode;
+        this.mCacheKey = TextUtils.isEmpty(api.mCacheKey) ? url().toString() : api.mCacheKey;
+
+        this.mConverter = api.mConverter;
+    }
+
+    public static SimpleUrlRequest.Api newApi(Url url, RequestMethod method) {
+        return new SimpleUrlRequest.Api(url, method);
+    }
+
+    @Override
+    public CacheMode cacheMode() {
+        return mCacheMode;
+    }
+
+    @Override
+    public String cacheKey() {
+        return mCacheKey;
+    }
+
+    @Override
+    public Converter converter() {
+        return mConverter;
+    }
+
+    public static class Api extends UrlRequest.Api<Api> {
+
+        private CacheMode mCacheMode;
+        private String mCacheKey;
+
+        private Converter mConverter;
+
+        private Api(Url url, RequestMethod method) {
+            super(url, method);
+        }
+
+        public Api cacheMode(CacheMode cacheMode) {
+            this.mCacheMode = cacheMode;
+            return this;
+        }
+
+        public Api cacheKey(String cacheKey) {
+            this.mCacheKey = cacheKey;
+            return this;
+        }
+
+        public Api converter(Converter converter) {
+            this.mConverter = converter;
+            return this;
+        }
+
+        public <S, F> SimpleResponse<S, F> perform(Type succeed, Type failed) throws Exception {
+            return RequestManager.getInstance().perform(new SimpleUrlRequest(this), succeed, failed);
+        }
+
+        public <S, F> Canceller perform(Callback<S, F> callback) {
+            return RequestManager.getInstance().perform(new SimpleUrlRequest(this), callback);
+        }
+    }
+}

+ 47 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/UrlWorker.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import com.yanzhenjie.kalle.Response;
+import com.yanzhenjie.kalle.connect.http.Call;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+final class UrlWorker<S, F> extends BasicWorker<SimpleUrlRequest, S, F> {
+
+    private Call mCall;
+
+    UrlWorker(SimpleUrlRequest request, Type succeed, Type failed) {
+        super(request, succeed, failed);
+    }
+
+    @Override
+    protected Response requestNetwork(SimpleUrlRequest request) throws IOException {
+        mCall = new Call(request);
+        return mCall.execute();
+    }
+
+    @Override
+    public void cancel() {
+        if (mCall != null && !mCall.isCanceled()) {
+            mCall.asyncCancel();
+        }
+    }
+}

+ 74 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/Work.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple;
+
+import com.yanzhenjie.kalle.Canceller;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/13.
+ */
+final class Work<T extends SimpleRequest, S, F> extends FutureTask<SimpleResponse<S, F>> implements Canceller {
+
+    private final Callback<S, F> mCallback;
+    private BasicWorker<T, S, F> mWorker;
+
+    Work(BasicWorker<T, S, F> work, Callback<S, F> callback) {
+        super(work);
+        this.mWorker = work;
+        this.mCallback = callback;
+    }
+
+    @Override
+    public void run() {
+        mCallback.onStart();
+        super.run();
+    }
+
+    @Override
+    protected void done() {
+        try {
+            mCallback.onResponse(get());
+        } catch (CancellationException e) {
+            mCallback.onCancel();
+        } catch (ExecutionException e) {
+            Throwable cause = e.getCause();
+            if (isCancelled()) {
+                mCallback.onCancel();
+            } else if (cause != null && cause instanceof Exception) {
+                mCallback.onException((Exception) cause);
+            } else {
+                mCallback.onException(new Exception(cause));
+            }
+        } catch (Exception e) {
+            if (isCancelled()) {
+                mCallback.onCancel();
+            } else {
+                mCallback.onException(e);
+            }
+        }
+        mCallback.onEnd();
+    }
+
+    @Override
+    public void cancel() {
+        cancel(true);
+        mWorker.cancel();
+    }
+}

+ 78 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/cache/Cache.java

@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple.cache;
+
+import com.yanzhenjie.kalle.Headers;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * CacheStore entity class.
+ * </p>
+ * Created in Jan 10, 2016 12:43:10 AM.
+ */
+public class Cache implements Serializable {
+
+    private String mKey;
+    private int mCode;
+    private Headers mHeaders;
+    private byte[] mBody;
+    private long mExpires;
+
+    public Cache() {
+    }
+
+    public String getKey() {
+        return mKey;
+    }
+
+    public void setKey(String key) {
+        this.mKey = key;
+    }
+
+    public int getCode() {
+        return mCode;
+    }
+
+    public void setCode(int code) {
+        mCode = code;
+    }
+
+    public Headers getHeaders() {
+        return mHeaders;
+    }
+
+    public void setHeaders(Headers headers) {
+        mHeaders = headers;
+    }
+
+    public byte[] getBody() {
+        return mBody;
+    }
+
+    public void setBody(byte[] body) {
+        this.mBody = body;
+    }
+
+    public long getExpires() {
+        return mExpires;
+    }
+
+    public void setExpires(long expires) {
+        this.mExpires = expires;
+    }
+}

+ 58 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/cache/CacheMode.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple.cache;
+
+/**
+ * Created by Zhenjie Yan on 2018/2/18.
+ */
+public enum CacheMode {
+    /**
+     * Follow the Http standard protocol.
+     */
+    HTTP,
+    /**
+     * Follow the Http standard protocol, but it will be cached.
+     */
+    HTTP_YES_THEN_WRITE_CACHE,
+    /**
+     * Only get the results from the network.
+     */
+    NETWORK,
+    /**
+     * Just get results from the network, and then decide whether to cache according to the Http protocol.
+     */
+    NETWORK_YES_THEN_HTTP,
+    /**
+     * Only get the results from the network, but it will be cached.
+     */
+    NETWORK_YES_THEN_WRITE_CACHE,
+    /**
+     * Get results first from the network, and from the cache if the network fails.
+     */
+    NETWORK_NO_THEN_READ_CACHE,
+    /**
+     * Just get the result from the cache.
+     */
+    READ_CACHE,
+    /**
+     * First get the result from the cache, if the cache does not exist, get the result from the network.
+     */
+    READ_CACHE_NO_THEN_NETWORK,
+    /**
+     * First get the result from the cache, if the cache does not exist, get results from the network, and follow the http protocol.
+     */
+    READ_CACHE_NO_THEN_HTTP
+}

+ 79 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/cache/CacheStore.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple.cache;
+
+/**
+ * <p>
+ * CacheStore interface.
+ * </p>
+ * Created in Dec 14, 2015 5:52:41 PM.
+ */
+public interface CacheStore {
+
+    CacheStore DEFAULT = new CacheStore() {
+        @Override
+        public Cache get(String key) {
+            return null;
+        }
+
+        @Override
+        public boolean replace(String key, Cache cache) {
+            return true;
+        }
+
+        @Override
+        public boolean remove(String key) {
+            return true;
+        }
+
+        @Override
+        public boolean clear() {
+            return true;
+        }
+    };
+
+    /**
+     * Get the cache.
+     *
+     * @param key unique key.
+     * @return cache.
+     */
+    Cache get(String key);
+
+    /**
+     * Save or set the cache.
+     *
+     * @param key   unique key.
+     * @param cache cache.
+     * @return cache.
+     */
+    boolean replace(String key, Cache cache);
+
+    /**
+     * Remove cache.
+     *
+     * @param key unique.
+     * @return cache.
+     */
+    boolean remove(String key);
+
+    /**
+     * Clear all data.
+     *
+     * @return returns true if successful, false otherwise.
+     */
+    boolean clear();
+}

+ 148 - 0
kalle/src/main/java/com/yanzhenjie/kalle/simple/cache/DiskCacheStore.java

@@ -0,0 +1,148 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.simple.cache;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.Headers;
+import com.yanzhenjie.kalle.secure.Encryption;
+import com.yanzhenjie.kalle.secure.Secret;
+import com.yanzhenjie.kalle.util.IOUtils;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.security.GeneralSecurityException;
+
+/**
+ * <p>
+ * Cache on disk.
+ * </p>
+ * Created by Zhenjie Yan on 2016/10/15.
+ */
+public class DiskCacheStore implements CacheStore {
+
+    private Secret mSecret;
+    private String mDirectory;
+    private DiskCacheStore(Builder builder) {
+        mDirectory = builder.mDirectory;
+        String password = TextUtils.isEmpty(builder.mPassword) ? mDirectory : builder.mPassword;
+        mSecret = Encryption.createSecret(password);
+    }
+
+    public static Builder newBuilder(String directory) {
+        return new Builder(directory);
+    }
+
+    @Override
+    public Cache get(String key) {
+        key = uniqueKey(key);
+
+        BufferedReader reader = null;
+        try {
+            File file = new File(mDirectory, key);
+            if (!file.exists() || file.isDirectory()) return null;
+
+            Cache cache = new Cache();
+            reader = new BufferedReader(new FileReader(file));
+            cache.setCode(Integer.parseInt(decrypt(reader.readLine())));
+            cache.setHeaders(Headers.fromJSONString(decrypt(reader.readLine())));
+            cache.setBody(Encryption.hexToByteArray(decrypt(reader.readLine())));
+            cache.setExpires(Long.parseLong(decrypt(reader.readLine())));
+            return cache;
+        } catch (Exception e) {
+            IOUtils.delFileOrFolder(new File(mDirectory, key));
+        } finally {
+            IOUtils.closeQuietly(reader);
+        }
+        return null;
+    }
+
+    @Override
+    public boolean replace(String key, Cache cache) {
+        key = uniqueKey(key);
+
+        BufferedWriter writer = null;
+        try {
+            if (TextUtils.isEmpty(key) || cache == null) return false;
+            if (!IOUtils.createFolder(mDirectory)) return false;
+
+            File file = new File(mDirectory, key);
+            if (!IOUtils.createNewFile(file)) return false;
+
+            writer = IOUtils.toBufferedWriter(new FileWriter(file));
+            writer.write(encrypt(Integer.toString(cache.getCode())));
+            writer.newLine();
+            writer.write(encrypt(Headers.toJSONString(cache.getHeaders())));
+            writer.newLine();
+            writer.write(encrypt(Encryption.byteArrayToHex(cache.getBody())));
+            writer.newLine();
+            writer.write(encrypt(Long.toString(cache.getExpires())));
+            writer.flush();
+            return true;
+        } catch (Exception e) {
+            IOUtils.delFileOrFolder(new File(mDirectory, key));
+        } finally {
+            IOUtils.closeQuietly(writer);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean remove(String key) {
+        key = uniqueKey(key);
+        return IOUtils.delFileOrFolder(new File(mDirectory, key));
+    }
+
+    @Override
+    public boolean clear() {
+        return IOUtils.delFileOrFolder(mDirectory);
+    }
+
+    private String encrypt(String encryptionText) throws GeneralSecurityException {
+        return mSecret.encrypt(encryptionText);
+    }
+
+    private String decrypt(String cipherText) throws GeneralSecurityException {
+        return mSecret.decrypt(cipherText);
+    }
+
+    private String uniqueKey(String key) {
+        return Encryption.getMD5ForString(mDirectory + key);
+    }
+
+    public static class Builder {
+
+        private String mDirectory;
+        private String mPassword;
+
+        private Builder(String directory) {
+            this.mDirectory = directory;
+        }
+
+        public Builder password(String password) {
+            this.mPassword = password;
+            return this;
+        }
+
+        public DiskCacheStore build() {
+            return new DiskCacheStore(this);
+        }
+    }
+
+}

+ 26 - 0
kalle/src/main/java/com/yanzhenjie/kalle/ssl/CompatSSLSocketFactory.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.ssl;
+
+/**
+ * <p>Add CipherSuites to the lower version.</p>
+ * Created by Zhenjie Yan on 2017/6/13.
+ *
+ * @deprecated use {@link TLSSocketFactory} instead.
+ */
+@Deprecated
+public class CompatSSLSocketFactory extends TLSSocketFactory {
+}

+ 34 - 0
kalle/src/main/java/com/yanzhenjie/kalle/ssl/SSLUtils.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.ssl;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Created by Zhenjie Yan on 2017/6/13.
+ */
+public class SSLUtils {
+
+    public static final HostnameVerifier HOSTNAME_VERIFIER = new HostnameVerifier() {
+        public boolean verify(String hostname, SSLSession session) {
+            return true;
+        }
+    };
+
+    public static final SSLSocketFactory SSL_SOCKET_FACTORY = new TLSSocketFactory();
+}

+ 142 - 0
kalle/src/main/java/com/yanzhenjie/kalle/ssl/TLSSocketFactory.java

@@ -0,0 +1,142 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.ssl;
+
+import android.os.Build;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Created by Zhenjie Yan on 2018/5/4.
+ */
+public class TLSSocketFactory extends SSLSocketFactory {
+
+    private static final String PROTOCOL_ARRAY[];
+    private static final X509TrustManager DEFAULT_TRUST_MANAGERS = new X509TrustManager() {
+
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String authType) {
+            // Trust.
+        }
+
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String authType) {
+            // Trust.
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[0];
+        }
+    };
+
+    static {
+        // https://developer.android.com/about/versions/android-5.0-changes.html#ssl
+        // https://developer.android.com/reference/javax/net/ssl/SSLSocket
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            PROTOCOL_ARRAY = new String[]{"TLSv1", "TLSv1.1", "TLSv1.2"};
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+            PROTOCOL_ARRAY = new String[]{"SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"};
+        } else {
+            PROTOCOL_ARRAY = new String[]{"SSLv3", "TLSv1"};
+        }
+    }
+
+    private SSLSocketFactory delegate;
+
+    public TLSSocketFactory() {
+        try {
+            SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, new TrustManager[]{DEFAULT_TRUST_MANAGERS}, new SecureRandom());
+            delegate = sslContext.getSocketFactory();
+        } catch (GeneralSecurityException e) {
+            throw new AssertionError(); // The system has no TLS. Just give up.
+        }
+    }
+
+    public TLSSocketFactory(SSLSocketFactory factory) {
+        this.delegate = factory;
+    }
+
+    private static void setSupportProtocolAndCipherSuites(Socket socket) {
+        if (socket instanceof SSLSocket) {
+            ((SSLSocket) socket).setEnabledProtocols(PROTOCOL_ARRAY);
+        }
+    }
+
+    @Override
+    public String[] getDefaultCipherSuites() {
+        return delegate.getDefaultCipherSuites();
+    }
+
+    @Override
+    public String[] getSupportedCipherSuites() {
+        return delegate.getSupportedCipherSuites();
+    }
+
+    @Override
+    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
+        Socket ssl = delegate.createSocket(s, host, port, autoClose);
+        setSupportProtocolAndCipherSuites(ssl);
+        return ssl;
+    }
+
+    @Override
+    public Socket createSocket(String host, int port) throws IOException {
+        Socket ssl = delegate.createSocket(host, port);
+        setSupportProtocolAndCipherSuites(ssl);
+        return ssl;
+    }
+
+    @Override
+    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
+        Socket ssl = delegate.createSocket(host, port, localHost, localPort);
+        setSupportProtocolAndCipherSuites(ssl);
+        return ssl;
+    }
+
+    @Override
+    public Socket createSocket(InetAddress host, int port) throws IOException {
+        Socket ssl = delegate.createSocket(host, port);
+        setSupportProtocolAndCipherSuites(ssl);
+        return ssl;
+    }
+
+    @Override
+    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
+        Socket ssl = delegate.createSocket(address, port, localAddress, localPort);
+        setSupportProtocolAndCipherSuites(ssl);
+        return ssl;
+    }
+
+    @Override
+    public Socket createSocket() throws IOException {
+        Socket ssl = delegate.createSocket();
+        setSupportProtocolAndCipherSuites(ssl);
+        return ssl;
+    }
+}

+ 99 - 0
kalle/src/main/java/com/yanzhenjie/kalle/urlconnect/URLConnection.java

@@ -0,0 +1,99 @@
+/*
+ * Copyright © 2018 Zhenjie Yan.
+ *
+ * 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.yanzhenjie.kalle.urlconnect;
+
+import android.text.TextUtils;
+
+import com.yanzhenjie.kalle.connect.Connection;
+import com.yanzhenjie.kalle.connect.stream.NullStream;
+import com.yanzhenjie.kalle.connect.stream.SourceStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * <p>
+ * Implement the network layer based on HttpURLConnection.
+ * </p>
+ * Created by Zhenjie Yan on 2017/2/12.
+ */
+public class URLConnection implements Connection {
+
+    private HttpURLConnection mConnection;
+
+    public URLConnection(HttpURLConnection connection) {
+        this.mConnection = connection;
+    }
+
+    private static InputStream getInputStream(String contentEncoding, InputStream stream) throws IOException {
+        if (!TextUtils.isEmpty(contentEncoding) && contentEncoding.contains("gzip")) {
+            stream = new GZIPInputStream(stream);
+        }
+        return stream;
+    }
+
+    private static boolean hasBody(String method, int code) {
+        return !"HEAD".equalsIgnoreCase(method) && hasBody(code);
+    }
+
+    private static boolean hasBody(int code) {
+        return code > 100 && code != 204 && code != 205 && !(code >= 300 && code < 400);
+    }
+
+    @Override
+    public OutputStream getOutputStream() throws IOException {
+        return mConnection.getOutputStream();
+    }
+
+    @Override
+    public int getCode() throws IOException {
+        return mConnection.getResponseCode();
+    }
+
+    @Override
+    public Map<String, List<String>> getHeaders() throws IOException {
+        return mConnection.getHeaderFields();
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        int code = mConnection.getResponseCode();
+        if (!hasBody(mConnection.getRequestMethod(), code)) return new NullStream(this);
+        if (code >= 400) {
+            return getInputStream(mConnection.getContentEncoding(), new SourceStream(this, mConnection.getErrorStream()));
+        }
+        return getInputStream(mConnection.getContentEncoding(), new SourceStream(this, mConnection.getInputStream()));
+    }
+
+    @Override
+    public void disconnect() {
+        if (mConnection != null) {
+            mConnection.disconnect();
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (mConnection != null) {
+            mConnection.disconnect();
+        }
+    }
+}

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels